commit afc2246e787af303c93b9447a4c20f15b8bfac6f Author: alwaysmesmerizingyou Date: Sun Oct 4 20:01:41 2026 -0700 Initial commit diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..81c4711 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..9fdbb82 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/capacitor.config.json b/capacitor.config.json new file mode 100644 index 0000000..a125c38 --- /dev/null +++ b/capacitor.config.json @@ -0,0 +1,9 @@ +{ + "appId": "tv.vyla.app", + "appName": "Vyla", + "webDir": ".", + "server": { + "androidScheme": "https", + "cleartext": true + } +} \ No newline at end of file diff --git a/css/base.css b/css/base.css new file mode 100644 index 0000000..3fa8652 --- /dev/null +++ b/css/base.css @@ -0,0 +1,315 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + outline: none; + -webkit-tap-highlight-color: transparent; + -webkit-user-select: none; + user-select: none; +} + +:root { + --pad: clamp(56px, 6vw, 132px); + --cw: clamp(120px, 9.4vw, 200px); +} + +html, +body { + height: 100%; + background: #07070a; + color: #f2f2f5; + font-family: "Inter", "SF Pro Display", "Segoe UI", Roboto, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + overflow: hidden; + cursor: default +} + +::-webkit-scrollbar { + display: none +} + +.f { + outline: none; + cursor: pointer +} + +.xf { + position: absolute; + inset: 0; + background-size: cover; + background-position: center top; + opacity: 0; + transition: opacity .7s ease +} + +.xf.in { + opacity: 1 +} + +#bg { + position: fixed; + inset: -40px; + background: #07070a; + filter: blur(42px) brightness(.55) saturate(1) +} + +#dim { + position: fixed; + inset: 0; + background: linear-gradient(180deg, rgba(0, 0, 0, .05) 0%, rgba(0, 0, 0, .55) 55%, #000 100%) +} + +#dim2 { + position: fixed; + inset: 0; + background: #000; + opacity: 0; + pointer-events: none +} + +#main { + position: fixed; + inset: 0; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: none +} + +.hero { + position: relative; + height: 60vh +} + +#hb { + position: absolute; + inset: 0; + -webkit-mask-image: linear-gradient(180deg, #000 50%, transparent 100%); + mask-image: linear-gradient(180deg, #000 50%, transparent 100%) +} + +#hb::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(90deg, rgba(0, 0, 0, .8) 0%, rgba(0, 0, 0, .35) 40%, transparent 70%) +} + +.hc { + position: absolute; + left: var(--pad); + bottom: 5vh; + width: min(48vw, 680px); + z-index: 2 +} + +#hlogo { + height: 16vh; + display: flex; + align-items: flex-end; + margin-bottom: 2vh +} + +#hlogo img { + max-width: min(34vw, 440px); + max-height: 16vh; + object-fit: contain; + filter: drop-shadow(0 4px 18px rgba(0, 0, 0, .6)) +} + +#hlogo h1 { + font-size: clamp(28px, 3.6vw, 64px); + font-weight: 700; + letter-spacing: -.02em; + line-height: 1.02; + text-shadow: 0 4px 24px rgba(0, 0, 0, .6) +} + +.meta { + display: flex; + gap: 14px; + align-items: center; + font-size: clamp(13px, 1.05vw, 19px); + color: #e3e3ee; + margin-bottom: 1.4vh; + font-weight: 500 +} + +.meta .b { + background: rgba(255, 255, 255, .18); + border-radius: 6px; + padding: 2px 8px; + font-size: .85em; + font-weight: 600 +} + +.ov1 { + font-size: clamp(13px, 1.1vw, 20px); + line-height: 1.5; + color: #d4d4e2; + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: 2.4vh; + max-width: 640px +} + +.hb { + display: flex; + gap: 14px; + align-items: center +} + +.pb, +.ib { + border: 0; + font: inherit; + font-weight: 600; + font-size: clamp(14px, 1.1vw, 20px); + border-radius: 999px; + height: clamp(44px, 3.6vw, 64px); + padding: 0 clamp(28px, 2.6vw, 48px); + display: inline-flex; + align-items: center; + gap: 10px; + transition: transform .2s, background .2s +} + +.pb { + background: #fff; + color: #000 +} + +.ib { + background: rgba(255, 255, 255, .16); + color: #fff; + backdrop-filter: blur(12px); + padding: 0 clamp(20px, 2vw, 36px) +} + +.pb.on, +.ib.on { + transform: scale(1.04); +} + +.ib.on { + background: rgba(255, 255, 255, .3) +} + +.sec { + padding-top: 10px +} + +.sec h2 { + padding: 0 var(--pad); + font-size: clamp(16px, 1.5vw, 26px); + font-weight: 700; + margin-bottom: 4px; + text-shadow: 0 2px 10px rgba(0, 0, 0, .6) +} + +.row { + display: flex; + gap: 16px; + overflow-x: auto; + overflow-y: hidden; + padding: 22px var(--pad) 26px; + position: relative; + scrollbar-width: none; + min-height: calc(var(--cw)*1.5 + 48px) +} + +.card { + flex: 0 0 var(--cw); + height: calc(var(--cw)*1.5); + border-radius: 14px; + overflow: hidden; + background: #1b1b24; + position: relative; + transform: translateZ(0); + transition: transform .38s cubic-bezier(.22, .9, .3, 1); + will-change: transform +} + +.card img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + filter: brightness(.86); + transition: filter .38s +} + +.card::after { + content: ""; + position: absolute; + inset: 0; + z-index: 3; + border-radius: inherit; + opacity: 0; + transition: opacity .25s; + pointer-events: none +} + +.card.on { + transform: scale(1.06) translateY(-4px); + z-index: 2 +} + +.card.on img { + filter: none +} + +.card.on::after { + opacity: 1 +} + +.kbm, +.kbm * { + cursor: none !important +} + +#sentinel { + height: 40vh +} + +.none { + color: rgba(255, 255, 255, .6); + padding: 5vh 0; + font-size: 18px +} + + +.toast { + position: fixed; + left: 50%; + bottom: 7vh; + transform: translateX(-50%); + z-index: 200; + background: #fff; + color: #000; + padding: 12px 22px; + border-radius: 999px; + font-weight: 600; + font-size: 15px; + animation: tin .25s both, tout .3s 1.9s forwards +} + +@keyframes tin { + from { + opacity: 0; + transform: translate(-50%, 14px) + } + + to { + opacity: 1; + transform: translate(-50%, 0) + } +} + +@keyframes tout { + to { + opacity: 0 + } +} \ No newline at end of file diff --git a/css/detail.css b/css/detail.css new file mode 100644 index 0000000..7a26f22 --- /dev/null +++ b/css/detail.css @@ -0,0 +1,432 @@ +#ov { + position: fixed; + inset: 0; + z-index: 50; + background: #000; + overflow-y: auto; + opacity: 0; + pointer-events: none; + transform: scale(1.04); + transition: opacity .35s, transform .35s; + scrollbar-width: none +} + +#ov.show { + opacity: 1; + pointer-events: auto; + transform: none +} + +.dbl { + position: fixed; + inset: -60px; + background-size: cover; + background-position: center; + filter: blur(60px) brightness(.5) saturate(1) +} + +.dw { + position: relative; + min-height: 100vh +} + +.dimg { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 82vh; + background-size: cover; + background-position: center top; + -webkit-mask-image: linear-gradient(180deg, #000 45%, transparent 100%); + mask-image: linear-gradient(180deg, #000 45%, transparent 100%) +} + +.dc { + position: relative; + padding: 40vh 0 8vh +} + +.dc>.dh { + padding: 0 6vw +} + +.dh #dlogo { + height: 20vh; + display: flex; + align-items: flex-end; + margin-bottom: 2vh +} + +.dh #dlogo img { + max-width: min(40vw, 540px); + max-height: 20vh; + object-fit: contain; + filter: drop-shadow(0 4px 18px rgba(0, 0, 0, .6)) +} + +.dh h1 { + font-size: clamp(30px, 4.4vw, 76px); + font-weight: 700; + letter-spacing: -.02em; + line-height: 1 +} + +.dh .meta { + margin-top: 1vh +} + +.dh .hb { + margin: 2vh 0 +} + +.circ { + width: clamp(44px, 3.6vw, 64px); + height: clamp(44px, 3.6vw, 64px); + border-radius: 50%; + background: rgba(255, 255, 255, .12); + backdrop-filter: blur(12px); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + font-weight: 700; +} + +.circ.on { + transform: scale(1.1); +} + +.otx { + max-width: min(70vw, 980px); + font-size: clamp(13px, 1.1vw, 20px); + line-height: 1.55; + color: #d9d9e4; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden +} + +.otx.x { + -webkit-line-clamp: unset; + line-clamp: unset; + display: block +} + +.more { + display: inline-block; + margin-top: 10px; + background: rgba(255, 255, 255, .18); + border-radius: 999px; + padding: 5px 14px; + font-size: 13px; + font-weight: 700; +} + +.more.on { + transform: scale(1.08); +} + +.ds { + margin-top: 3vh +} + +.ds h3 { + padding: 0 6vw; + font-size: clamp(17px, 1.5vw, 26px); + font-weight: 700 +} + +.ds .row { + padding-left: 6vw; + padding-right: 6vw; + min-height: 0 +} + +.tr { + flex: 0 0 clamp(250px, 21vw, 400px); + border-radius: 20px; + position: relative; + overflow: hidden; + background: #101016; + padding: 0 0 18px; + isolation: isolate; +} + +.tr .tb { + position: absolute; + inset: -40px; + background-size: cover; + background-position: center; + filter: blur(34px) brightness(.62) saturate(1.35); + transform: scale(1.2); + z-index: -1 +} + +.tr .th { + aspect-ratio: 16/9; + background-size: cover; + background-position: center; + margin: 0 0 -26px; + -webkit-mask-image: linear-gradient(180deg, #000 50%, rgba(0, 0, 0, .55) 74%, transparent 100%); + mask-image: linear-gradient(180deg, #000 50%, rgba(0, 0, 0, .55) 74%, transparent 100%) +} + +.tr b, +.tr small { + display: block; + padding: 0 18px; + position: relative +} + +.tr b { + font-size: clamp(14px, 1.1vw, 19px); + font-weight: 600 +} + +.tr small { + color: #d0d0da; + margin-top: 5px +} + +.tr.on { + transform: scale(1.04); +} + +.cs { + flex: 0 0 clamp(96px, 7.6vw, 150px); + text-align: center; + transition: transform .22s +} + +.cs .pf { + width: clamp(90px, 7vw, 130px); + height: clamp(90px, 7vw, 130px); + border-radius: 50%; + margin: 0 auto 10px; + background: #2a2a3c center/cover; + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; + font-weight: 700; +} + +.cs b { + display: block; + font-size: clamp(12px, .95vw, 16px) +} + +.cs small { + display: block; + color: #aaa; + font-size: clamp(11px, .8vw, 14px); + margin-top: 2px +} + +.cs.on { + transform: scale(1.07) +} + +.circ { + position: relative +} + +.circ svg { + width: 46%; + height: 46% +} + +.circ.sv { + background: #fff; + color: #000 +} + +#dfv.sv svg { + fill: currentColor +} + +.circ[data-l].on::after { + content: attr(data-l); + position: absolute; + top: calc(100% + 12px); + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + font-size: 13px; + font-weight: 600; + background: #fff; + color: #000; + padding: 5px 12px; + border-radius: 999px; + z-index: 5; + pointer-events: none +} + +.mx { + display: none; + gap: 14px; + align-items: center +} + +.mx.open { + display: flex +} + +.mx.open .circ { + animation: pop .32s cubic-bezier(.22, .9, .3, 1) backwards +} + +.mx.open .circ+.circ { + animation-delay: .06s +} + +@keyframes pop { + from { + opacity: 0; + transform: scale(.4) translateX(-24px) + } +} + +.eps { + padding: 0 6vw +} + +.eh { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 14px +} + +.eps .eh h3 { + padding: 0 +} + +.dd { + position: relative; + z-index: 6 +} + +.ddb { + display: flex; + align-items: center; + gap: 10px; + height: 42px; + padding: 0 18px; + border-radius: 999px; + background: rgba(255, 255, 255, .16); + backdrop-filter: blur(12px); + font-weight: 600; + font-size: clamp(13px, 1vw, 17px); + transition: background .2s, color .2s, transform .25s +} + +.ddb svg { + width: 16px; + height: 16px +} + +.ddb.on { + background: #fff; + color: #000; + transform: scale(1.04) +} + +.ddl { + display: none; + position: absolute; + top: calc(100% + 8px); + left: 0; + min-width: 240px; + max-height: 46vh; + overflow-y: auto; + padding: 6px; + border-radius: 16px; + background: rgba(22, 22, 30, .97); + scrollbar-width: none +} + +.ddl.open { + display: block +} + +.ddi { + display: flex; + justify-content: space-between; + gap: 24px; + padding: 12px 14px; + border-radius: 10px; + font-weight: 500; + white-space: nowrap +} + +.ddi small { + color: rgba(255, 255, 255, .5) +} + +.ddi.on { + background: #fff; + color: #000 +} + +.ddi.on small { + color: rgba(0, 0, 0, .55) +} + +.el { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(clamp(210px, 17vw, 320px), 1fr)); + gap: 28px 20px; + padding: 8px 6px 14px; + margin: 0 -6px +} + +.el .none { + grid-column: 1/-1 +} + +.ep { + min-width: 0 +} + +.ep .et { + position: relative; + aspect-ratio: 16/9; + border-radius: 10px; + overflow: hidden; + background: #000; +} + +.ep .et img { + width: 100%; + height: 100%; + object-fit: cover; + display: block +} + +.ep b { + display: block; + margin-top: 12px; + font-size: clamp(13px, 1vw, 16px); + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis +} + +.ep small { + display: block; + margin-top: 3px; + font-size: clamp(12px, .9vw, 14px); + color: rgba(255, 255, 255, .5); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis +} + +.ep.on .et { + transform: scale(1.03) +} \ No newline at end of file diff --git a/css/library.css b/css/library.css new file mode 100644 index 0000000..10ab6a8 --- /dev/null +++ b/css/library.css @@ -0,0 +1,65 @@ +#lib { + position: fixed; + inset: 0; + z-index: 25; + background: linear-gradient(180deg, rgba(0, 0, 0, .25), rgba(0, 0, 0, .7)); + overflow-y: auto; + opacity: 0; + pointer-events: none; + transition: opacity .35s; + padding: 9vh var(--pad) 10vh; + scrollbar-width: none +} + +#lib.show { + opacity: 1; + pointer-events: auto +} + +.lib #main { + visibility: hidden +} + +.lib #bg { + filter: blur(70px) brightness(.5) saturate(1.5) +} + +.lib #dim { + opacity: .35 +} + +.lib #dim2 { + opacity: 0 !important +} + +#lib h1 { + font-size: clamp(28px, 3.4vw, 56px); + font-weight: 700; + letter-spacing: -.02em; + display: flex; + align-items: baseline; + gap: 18px +} + +#lib h1 small { + font-size: clamp(13px, 1.1vw, 19px); + font-weight: 500; + color: rgba(255, 255, 255, .6); + letter-spacing: 0 +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--cw), 1fr)); + gap: 22px; + padding: 30px 10px +} + +.grid .card { + height: auto; + aspect-ratio: 2/3 +} + +.grid .none { + grid-column: 1/-1 +} \ No newline at end of file diff --git a/css/player.css b/css/player.css new file mode 100644 index 0000000..c69008c --- /dev/null +++ b/css/player.css @@ -0,0 +1,715 @@ +#pl { + position: fixed; + inset: 0; + z-index: 80; + background: #000; + opacity: 0; + pointer-events: none; + transition: opacity .35s; +} + +#pl.show { + opacity: 1; + pointer-events: auto; +} + +#pl:not(.ui) { + cursor: none; +} + +#pv { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: contain; + background: #000; + z-index: 1; +} + +#pl.cover #pv { + object-fit: cover; +} + +video::cue { + font: 600 clamp(16px, 1.8vw, 30px) Inter, sans-serif; + background: rgba(0, 0, 0, .55); + color: #fff; +} + +.sub-active::cue { + color: var(--sub-color, #ffffff) !important; + background-color: var(--sub-bg, rgba(0, 0, 0, 0.75)) !important; + font-size: var(--sub-size, 22px) !important; + text-shadow: var(--sub-shadow, 0 2px 4px rgba(0, 0, 0, 0.8)) !important; +} + +.sub-ctrl-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 14px; + font-size: 14px; +} + +.sub-ctrl-row span { + color: rgba(255, 255, 255, 0.8); +} + +.sub-btn-group { + display: flex; + gap: 6px; +} + +.sub-btn-group button { + background: rgba(255, 255, 255, 0.15); + border: none; + color: #fff; + padding: 4px 10px; + border-radius: 6px; + cursor: pointer; + font-size: 12px; +} + +.sub-btn-group button.active { + background: #fff; + color: #000; + font-weight: 600; +} + +.src-group { + display: flex; + flex-direction: column; + margin-bottom: 4px; +} + +.src-header { + display: flex; + align-items: center; + justify-content: space-between; + height: 44px; + padding: 0 14px; + border-radius: 12px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + background: rgba(255, 255, 255, 0.08); + transition: background 0.15s, color 0.15s; +} + +.src-header.on, +.src-header:hover { + background: #fff; + color: #000; +} + +.src-expand-btn { + font-size: 12px; + padding: 2px 8px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.16); + color: inherit; + margin-left: 8px; +} + +.src-sublist { + display: none; + flex-direction: column; + gap: 2px; + padding-left: 14px; + margin: 4px 0; +} + +.src-sublist.open { + display: flex; +} + +.src-subitem { + display: flex; + align-items: center; + justify-content: space-between; + height: 38px; + padding: 0 12px; + border-radius: 8px; + font-size: 13px; + background: rgba(255, 255, 255, 0.04); + cursor: pointer; +} + +.src-subitem.on, +.src-subitem:hover { + background: rgba(255, 255, 255, 0.25); + color: #fff; +} + +#pl .top, +#pl .bot, +#pmid { + opacity: 0; + transition: opacity .3s, transform .25s, background .2s; +} + +#pl.ui .top, +#pl.ui .bot, +#pl.ui #pmid { + opacity: 1; +} + +#pl .f { + pointer-events: auto; +} + +#pl:not(.ui) .f { + pointer-events: none; +} + +#pl .top, +#pl .bot { + position: absolute; + left: 0; + right: 0; + pointer-events: none; +} + +#pl .top { + top: 0; + padding: 2.6vh 2.4vw 14vh; + display: flex; + align-items: flex-start; + background: linear-gradient(180deg, rgba(0, 0, 0, .65), transparent); +} + +#pl .bot { + bottom: 0; + padding: 10vh 2.4vw 2.6vh; + display: flex; + align-items: center; + gap: 18px; + background: linear-gradient(0deg, rgba(0, 0, 0, .7), transparent); +} + +.pic { + width: clamp(44px, 3.3vw, 56px); + height: clamp(44px, 3.3vw, 56px); + flex: 0 0 auto; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + transition: background .2s, color .2s, transform .2s; +} + +.pic svg { + width: 52%; + height: 52%; +} + +.pic.on { + background: #fff; + color: #000; + transform: scale(1.08); +} + +.av { + width: 46px; + height: 46px; + margin: 3px 12px 0 6px; + border-radius: 50%; + object-fit: cover; + background: #2a2a34; +} + +.pt { + height: 46px; + margin-top: 3px; + min-width: 0; + display: flex; + flex-direction: column; + justify-content: center; +} + +.pt b { + font-size: clamp(15px, 1.2vw, 19px); + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 36vw; +} + +.pt small { + font-size: clamp(12px, .9vw, 14px); + color: rgba(255, 255, 255, .7); + margin-top: 2px; +} + +.pr { + margin-left: auto; + display: flex; + align-items: center; + gap: 8px; +} + +#pmid { + position: absolute; + left: 50%; + top: 50%; + width: 112px; + height: 112px; + margin: -56px 0 0 -56px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; +} + +#pmid svg { + width: 72px; + height: 72px; + filter: drop-shadow(0 2px 14px rgba(0, 0, 0, .45)); +} + +#pmid.on { + background: rgba(255, 255, 255, .18); + transform: scale(1.08); +} + +.bt { + font-size: 14px; + font-variant-numeric: tabular-nums; + min-width: 44px; + color: #fff; +} + +#pdur { + text-align: right; +} + +.sk { + flex: 1; + height: 40px; + position: relative; +} + +.sk::before, +.sk u, +.sk i { + content: ""; + position: absolute; + left: 0; + top: 50%; + height: 4px; + margin-top: -2px; + border-radius: 2px; + transition: height .15s, margin .15s; +} + +.sk::before { + right: 0; + background: rgba(255, 255, 255, .2); +} + +.sk u { + width: 0; + background: rgba(255, 255, 255, .38); +} + +.sk i { + width: 0; + background: #fff; +} + +.sk b { + position: absolute; + top: 50%; + left: 0; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + border-radius: 50%; + background: #fff; + transition: transform .15s; +} + +.sk.on::before, +.sk.on u, +.sk.on i { + height: 6px; + margin-top: -3px; +} + +.sk.on b { + transform: scale(1.35); +} + +#pm { + display: none; + position: absolute; + top: calc(2.6vh + 66px); + right: 2.4vw; + width: 320px; + max-height: 70vh; + overflow-y: auto; + padding: 10px; + border-radius: 18px; + background: rgba(0, 0, 0, .94); + backdrop-filter: blur(24px); + z-index: 5; + pointer-events: auto; + scrollbar-width: none; +} + +#pm.open { + display: block; +} + +#pm h4 { + font-size: 12px; + font-weight: 600; + letter-spacing: .08em; + text-transform: uppercase; + color: rgba(255, 255, 255, .5); + padding: 10px 12px 6px; +} + +#pm p { + font-size: 11px; + line-height: 1.4; + color: rgba(255, 255, 255, .4); + padding: 10px 12px 4px; +} + +.pi { + display: flex; + align-items: center; + justify-content: space-between; + height: 48px; + padding: 0 14px; + border-radius: 12px; + font-size: 15px; + font-weight: 500; + transition: background .15s, color .15s; +} + +.pi svg { + width: 20px; + height: 20px; +} + +.pi.on { + background: #fff; + color: #000; +} + +.vrow { + display: flex; + align-items: center; + gap: 10px; + padding: 4px 6px 8px; +} + +.vrow span { + width: 30px; + text-align: right; + font-size: 13px; + color: rgba(255, 255, 255, .7); +} + +.vs { + flex: 1; + height: 36px; + position: relative; + border-radius: 8px; +} + +.vs::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 50%; + height: 4px; + margin-top: -2px; + border-radius: 2px; + background: rgba(255, 255, 255, .2); +} + +.vs i { + position: absolute; + left: 0; + top: 50%; + height: 4px; + margin-top: -2px; + border-radius: 2px; + background: #fff; +} + +.skip { + display: none; + height: 48px; + padding: 0 26px; + border-radius: 999px; + background: rgba(255, 255, 255, .18); + backdrop-filter: blur(16px); + align-items: center; + font-weight: 600; + transition: transform .25s, background .2s, color .2s; +} + +.skip.v { + display: flex; +} + +.skip.on { + background: #fff; + color: #000; + transform: scale(1.06); +} + +#pact { + position: absolute; + right: 2.4vw; + bottom: 16vh; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 12px; +} + +.skip svg { + width: 18px; + height: 18px; + margin-left: 10px; +} + +#pnext { + background: #fff; + color: #000; +} + +#pnext.on { + color: #000; +} + +#pl.eps .bot, +#pl.eps #pmid, +#pl.eps #pact { + opacity: 0 !important; + visibility: hidden; + pointer-events: none; +} + +#pep { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 9vh 0 3.4vh; + background: linear-gradient(0deg, rgba(0, 0, 0, .97) 60%, rgba(0, 0, 0, .8) 82%, transparent); + transform: translateY(40px); + opacity: 0; + visibility: hidden; + transition: transform .45s cubic-bezier(.22, .9, .3, 1), opacity .3s, visibility 0s .45s; +} + +#pl.eps #pep { + transform: none; + opacity: 1; + visibility: visible; + transition-delay: 0s; +} + +.peh { + display: flex; + align-items: center; + gap: 22px; + padding: 0 2.4vw; +} + +.peh h3 { + font-size: clamp(18px, 1.6vw, 26px); + font-weight: 700; + display: flex; + align-items: baseline; + gap: 14px; + white-space: nowrap; +} + +.peh h3 small { + font-size: .62em; + font-weight: 500; + color: rgba(255, 255, 255, .55); + max-width: 22vw; + overflow: hidden; + text-overflow: ellipsis; +} + +#pep #pseas { + flex: 1; + min-width: 0; + padding: 4px; + margin: 0; + min-height: 0; + gap: 10px; +} + +#pep .chip { + height: 38px; + padding: 0 18px; + font-size: 14px; + background: rgba(255, 255, 255, .12); +} + +#pep .chip.act { + background: rgba(255, 255, 255, .3); +} + +#pep .chip.on { + background: #fff; + color: #000; + transform: scale(1.05); +} + +#pep .pcar { + padding: 18px 2.4vw 8px; + min-height: 0; + gap: 18px; + -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 5vw), transparent); + mask-image: linear-gradient(90deg, #000 calc(100% - 5vw), transparent); +} + +.pe { + flex: 0 0 clamp(230px, 19vw, 330px); + min-width: 0; +} + +.pe .et { + position: relative; + aspect-ratio: 16/9; + border-radius: 12px; + overflow: hidden; + background: #000; +} + +.pe .et img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.pe .et>i { + position: absolute; + left: 10px; + top: 10px; + font-style: normal; + font-size: 12px; + font-weight: 700; + padding: 3px 9px; + border-radius: 999px; + background: rgba(0, 0, 0, .6); + backdrop-filter: blur(8px); +} + +.pe .pn { + display: block; + margin-top: 11px; + font-size: clamp(13px, 1vw, 16px); + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.pe small { + display: block; + margin-top: 3px; + font-size: 13px; + color: rgba(255, 255, 255, .5); +} + +.pe.on .et { + transform: scale(1.04); +} + +.pe .pg { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 4px; + background: rgba(255, 255, 255, .22); +} + +.pe .pg u, +.eq u { + display: block; + text-decoration: none; +} + +.pe .pg u { + height: 100%; + width: var(--p, 0%); +} + +.eq { + position: absolute; + right: 10px; + bottom: 14px; + display: flex; + gap: 3px; + align-items: flex-end; + height: 16px; +} + +.eq u { + width: 3px; + height: 4px; + border-radius: 2px; + animation: eq 1s ease-in-out infinite; +} + +.eq u:nth-child(2) { + animation-delay: .2s; +} + +.eq u:nth-child(3) { + animation-delay: .4s; +} + +@keyframes eq { + 50% { + height: 16px; + } +} + +#pbg { + position: absolute; + inset: -60px; + width: calc(100% + 120px); + height: calc(100% + 120px); + object-fit: cover; + filter: blur(54px) brightness(0.42) saturate(1.45); + transform: scale(1.08); + pointer-events: none; + z-index: 0; + opacity: 1; + transition: opacity 0.4s ease; +} + +#pl.cover #pbg { + opacity: 0; + visibility: hidden; +} + +#pv { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: contain; + background: transparent; + z-index: 1; +} \ No newline at end of file diff --git a/css/search.css b/css/search.css new file mode 100644 index 0000000..33d5178 --- /dev/null +++ b/css/search.css @@ -0,0 +1,268 @@ +.srch #main { + visibility: hidden +} + +.srch #bg { + filter: blur(70px) brightness(.5) saturate(1.5) +} + +.srch #dim { + opacity: .35 +} + +.srch #dim2 { + opacity: 0 !important +} + +#sr { + position: fixed; + inset: 0; + z-index: 25; + background: linear-gradient(180deg, rgba(0, 0, 0, .2), rgba(0, 0, 0, .6)); + overflow-y: auto; + opacity: 0; + pointer-events: none; + transition: opacity .35s; + padding: 6vh var(--pad) 8vh; + scrollbar-width: none +} + +#sr.show { + opacity: 1; + pointer-events: auto +} + +.stop { + display: flex; + align-items: center; + gap: 18px; + height: clamp(46px, 4.6vw, 76px) +} + +.stop svg { + width: clamp(30px, 2.8vw, 46px); + height: clamp(30px, 2.8vw, 46px); + color: rgba(255, 255, 255, .85); + flex: 0 0 auto +} + +#stext { + font-size: clamp(28px, 3vw, 52px); + font-weight: 600; + letter-spacing: -.02em; + color: rgba(255, 255, 255, .5); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 70vw +} + +#stext.q { + color: #fff +} + +#stext.q::after { + content: ""; + display: inline-block; + width: 3px; + height: .8em; + background: #fff; + margin-left: 5px; + vertical-align: -.08em; + animation: bl 1s steps(1) infinite +} + +@keyframes bl { + 50% { + opacity: 0 + } +} + +#kb { + display: flex; + align-items: center; + justify-content: space-between; + gap: clamp(2px, .35vw, 8px); + margin: 1.4vh 0 0 4px; + width: min(100%, 1180px) +} + +.k { + flex: 1 1 0; + min-width: 0; + max-width: 46px; + height: clamp(34px, 3vw, 52px); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: clamp(16px, 1.6vw, 26px); + font-weight: 500; + color: rgba(255, 255, 255, .88); + transition: background .2s, color .2s, transform .22s cubic-bezier(.22, .9, .3, 1) +} + +.k.w { + flex: 0 0 auto; + max-width: none; + font-size: clamp(9px, .8vw, 13px); + font-weight: 700; + padding: 0 clamp(8px, .8vw, 14px); + background: rgba(255, 255, 255, .22); + letter-spacing: .04em; + height: clamp(26px, 2.3vw, 40px) +} + +.k.d svg { + width: 1.1em; + height: 1.1em +} + +.k.on { + background: #fff; + color: #000; + transform: scale(1.22); +} + +.row.ch { + padding: 16px 6px 18px; + min-height: 0; + margin: 0 -6px; + gap: 14px +} + +.chip { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 8px; + padding: 0 20px; + height: clamp(36px, 3vw, 52px); + border-radius: 12px; + background: rgba(255, 255, 255, .24); + color: #fff; + font-size: clamp(14px, 1.3vw, 22px); + font-weight: 500; + white-space: nowrap; + transition: background .2s, color .2s, transform .22s cubic-bezier(.22, .9, .3, 1) +} + +.chip svg { + width: 1.1em; + height: 1.1em +} + +.chip.on { + background: #fff; + color: #000; + transform: scale(1.06) +} + +.tabs { + display: flex; + justify-content: space-around; + border-top: 1px solid rgba(255, 255, 255, .22); + margin-top: 1vh; + padding-top: 1.6vh +} + +.tab { + padding: 8px 24px; + border-radius: 999px; + font-size: clamp(13px, 1.15vw, 19px); + color: rgba(255, 255, 255, .65); + position: relative; + transition: background .2s, color .2s +} + +.tab.act { + color: #fff; + font-weight: 600 +} + +.tab.act::after { + content: ""; + position: absolute; + left: 24px; + right: 24px; + bottom: 0; + height: 2px; + background: #fff; + border-radius: 2px +} + +.tab.on { + background: rgba(255, 255, 255, .2); + color: #fff +} + +.ss { + margin-top: 2.6vh +} + +.ss h3 { + font-size: clamp(14px, 1.2vw, 20px); + font-weight: 500; + color: rgba(255, 255, 255, .75) +} + + + + + + + + +.sc { + flex: 0 0 var(--cw); + transition: transform .32s cubic-bezier(.22, .9, .3, 1) +} + +.sc .po { + aspect-ratio: 2/3; + border-radius: 14px; + overflow: hidden; + background: #1b1b24; +} + +.sc .po img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + filter: brightness(.86); + transition: filter .3s +} + +.sc b { + display: block; + margin-top: 10px; + font-size: clamp(13px, 1vw, 17px); + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis +} + +.sc small { + display: flex; + gap: 8px; + margin-top: 3px; + color: rgba(255, 255, 255, .62); + font-size: clamp(11px, .85vw, 14px); + white-space: nowrap +} + +.sc small span+span::before { + content: "\2022"; + margin-right: 8px; + opacity: .5 +} + +.sc.on { + transform: scale(1.05) translateY(-3px) +} + +.sc.on img { + filter: none +} \ No newline at end of file diff --git a/css/sidebar.css b/css/sidebar.css new file mode 100644 index 0000000..00e6092 --- /dev/null +++ b/css/sidebar.css @@ -0,0 +1,114 @@ +#side { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: 340px; + z-index: 30; + pointer-events: none +} + +#side::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(90deg, rgba(0, 0, 0, .92) 0%, rgba(0, 0, 0, .7) 55%, transparent 100%); + opacity: 0; + transition: opacity .35s +} + +#side.open::before { + opacity: 1 +} + +#side nav { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + display: flex; + flex-direction: column; + gap: 6px; + pointer-events: auto +} + +.ni { + margin-left: 16px; + width: 56px; + height: 56px; + border-radius: 28px; + display: flex; + align-items: center; + gap: 18px; + padding: 0 16px; + color: #fff; + opacity: .45; + overflow: hidden; + white-space: nowrap; + transition: width .32s cubic-bezier(.2, .8, .2, 1), background .25s, color .25s, opacity .25s +} + +.ni svg { + width: 24px; + height: 24px; + flex: 0 0 24px +} + +.ni span { + font-size: 19px; + font-weight: 600; + opacity: 0; + transition: opacity .25s +} + +.ni.act { + opacity: 1 +} + +#side.open .ni { + width: 260px; + opacity: .85 +} + +#side.open .ni span { + opacity: 1 +} + +#side.open .ni.on { + background: #fff; + color: #000; + opacity: 1 +} + +.brand { + position: absolute; + left: 24px; + top: 5vh; + display: flex; + align-items: center; + gap: 14px; + height: 44px; + color: #fff +} + +.brand svg, +.brand img { + width: 40px; + height: 40px; + flex: 0 0 40px; + object-fit: contain +} + +.brand span { + font-size: 24px; + font-weight: 700; + letter-spacing: -.03em; + opacity: 0; + transform: translateX(-6px); + transition: opacity .3s, transform .3s +} + +#side.open .brand span { + opacity: 1; + transform: none +} \ No newline at end of file diff --git a/css/skeleton.css b/css/skeleton.css new file mode 100644 index 0000000..2a88d24 --- /dev/null +++ b/css/skeleton.css @@ -0,0 +1,213 @@ +.sb, +.card:not(.lded), +.sc .po:not(.lded), +.ep .et:not(.lded), +.cs.sl .pf, +.trk, +.chip.sl { + position: relative; + overflow: hidden; + background: #15151c +} + +.sb::before, +.card:not(.lded)::before, +.sc .po:not(.lded)::before, +.ep .et:not(.lded)::before, +.cs.sl .pf::before, +.trk::before, +.chip.sl::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(100deg, transparent 25%, rgba(255, 255, 255, .075) 50%, transparent 75%); + transform: translateX(-100%); + animation: shim 1.5s ease-in-out infinite +} + +@keyframes shim { + to { + transform: translateX(100%) + } +} + +.card img, +.sc .po img, +.ep .et img { + opacity: 0; + transition: opacity .45s ease, filter .38s +} + +.card img.ld, +.sc .po img.ld, +.ep .et img.ld { + opacity: 1 +} + +.sb { + display: block; + border-radius: 8px; + height: 14px +} + +.sec>.sb.h2 { + width: 220px; + height: 22px; + margin: 0 var(--pad) 4px +} + +.ds>.sb.h3 { + width: 180px; + height: 22px; + margin: 0 6vw 6px +} + +.ss>.sb.h3s { + width: 160px; + height: 18px +} + +.sc.sl .sb { + width: 80%; + height: 13px; + margin-top: 10px +} + +.sc.sl .sb.s { + width: 50%; + height: 11px; + margin-top: 6px +} + +.chip.sl { + height: clamp(36px, 3vw, 52px) +} + +.trk { + flex: 0 0 clamp(250px, 21vw, 400px); + aspect-ratio: 16/10; + border-radius: 20px +} + +.cs.sl { + flex: 0 0 clamp(96px, 7.6vw, 150px) +} + +.hskel { + display: none +} + +.hc.sl .hskel { + display: block +} + +.hc.sl>:not(.hskel) { + display: none +} + +.hskel .t, +.dsk .t { + width: min(32vw, 400px); + height: 10vh; + border-radius: 14px; + margin-bottom: 3vh +} + +.hskel .m, +.dsk .m { + width: min(22vw, 300px); + height: 18px; + margin-bottom: 2vh +} + +.hskel .l, +.dsk .l { + width: min(44vw, 620px); + height: 15px; + margin-bottom: 10px +} + +.hskel .l.x, +.dsk .l.x { + width: min(26vw, 360px) +} + +.hskel .hb, +.dsk .hb { + margin: 3vh 0 +} + +.hskel .b, +.dsk .b { + width: clamp(130px, 11vw, 190px); + height: clamp(44px, 3.6vw, 64px); + border-radius: 999px +} + +.hskel .b.c, +.dsk .c { + width: clamp(44px, 3.6vw, 64px); + border-radius: 50% +} + +.dw { + animation: fin .4s ease +} + +@keyframes fin { + from { + opacity: 0 + } +} + +#pspin { + position: absolute; + left: 50%; + top: 50%; + width: 64px; + height: 64px; + margin: -32px 0 0 -32px; + border-radius: 50%; + border: 3px solid rgba(255, 255, 255, .2); + border-top-color: #fff; + animation: spin .8s linear infinite; + opacity: 0; + pointer-events: none; + transition: opacity .2s +} + +#pl.buf #pspin { + opacity: 1 +} + +#pl.buf #pmid { + opacity: 0 !important +} + +@keyframes spin { + to { + transform: rotate(360deg) + } +} + +.pe .et:not(.lded)::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(100deg, transparent 25%, rgba(255, 255, 255, .075) 50%, transparent 75%); + transform: translateX(-100%); + animation: shim 1.5s ease-in-out infinite +} + +.pe .et:not(.lded) { + background: #15151c +} + +.pe .et img { + opacity: 0; + transition: opacity .45s ease +} + +.pe .et img.ld { + opacity: 1 +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..60f5dc1 --- /dev/null +++ b/index.html @@ -0,0 +1,90 @@ + + + + + + + Vyla + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+ +
+

+
+
+
+
+
+
+
+
+ + + Search
+
+
+
+
+
+
+

+
+
+ +
+
+ + +
+
+
+ + +
+
+
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/browse.js b/js/browse.js new file mode 100644 index 0000000..f5be750 --- /dev/null +++ b/js/browse.js @@ -0,0 +1,57 @@ +function hero(it) { clearTimeout(ht); ht = setTimeout(() => applyHero(it), 130) } +async function applyHero(it) { + if (heroIt === it) return; heroIt = it; $('.hc').classList.remove('sl'); + const bp = it.backdrop_path || it.poster_path; + xf(bgb, I + 'w780' + bp); xf(hb, I + 'w1280' + bp); + const t = it.title || it.name; + $('#hlogo').innerHTML = `

${t}

`; + $('#hmeta').innerHTML = `★ ${(it.vote_average || 0).toFixed(1)}${yr(it)}${it._t === 'movie' ? 'Movie' : 'Series'}${gn(it)}`; + $('#hov').textContent = it.overview || ''; + const l = await logo(it); + if (heroIt === it && l) $('#hlogo').innerHTML = `${t.replace(/` +} + +function card(it, h = true) { + const c = document.createElement('div'); c.className = 'card f'; c.tabIndex = -1; + c.innerHTML = ``; + if (h) c._it = it; c._a = () => openDetail(it); return c +} + +function mkRow(i) { + const mv = mode === "movie" || (mode === "discover" && i % 2 === 0), k = mode === "discover" ? i >> 1 : i, list = mv ? MG : TG, g = list[k % list.length], pg = 1 + Math.floor(k / list.length) % 20; + const sec = document.createElement('section'); sec.className = 'sec'; + sec.innerHTML = `

${g.name} ${mv ? 'Movies' : 'TV Shows'}

${SK.cards(14)}
`; + rowsEl.appendChild(sec); + return api(`/discover/${mv ? 'movie' : 'tv'}`, `&with_genres=${g.id}&sort_by=popularity.desc&vote_count.gte=100&include_adult=false&page=${pg}`).then(d => { + const rs = (d.results || []).filter(x => x.poster_path); + if (!rs.length) { sec.remove(); return } + sec.lastChild.innerHTML = ''; rs.forEach(x => { x._t = mv ? 'movie' : 'tv'; sec.lastChild.appendChild(card(x)) }) + }) +} + +async function more(n) { + if (busy) return; busy = true; const g = gen; + const ps = []; for (let j = 0; j < n; j++)ps.push(mkRow(idx++)); + await Promise.all(ps); if (g === gen) busy = false +} + +async function go(k) { + if (k === 'search') { closeLib(true); openSearch(); return } + closeSearch(true); + if (k === 'wl' || k === 'fav') { openLib(k); return } + closeLib(true); + if (k === mode && rowsEl.children.length) { setAct(k); setFocus(document.querySelector('#rows .card')); return } + mode = k; setAct(k); gen++; busy = false; idx = 0; heroIt = null; lastMain = null; rowsEl.innerHTML = ''; main.scrollTop = 0; $('.hc').classList.add('sl'); + await more(6); + const first = document.querySelector('#rows .card'); + if (first) { applyHero(first._it); setFocus(first) } +} + +main.addEventListener('scroll', () => { + const h = innerHeight; + $('#dim2').style.opacity = Math.min(main.scrollTop / (h * .6), 1) * .55; + if (main.scrollTop + h * 2.5 > main.scrollHeight) more(4) +}); + +$('#hplay')._a = () => openPlayer(heroIt); +$('#hinfo')._a = () => heroIt && openDetail(heroIt); diff --git a/js/core.js b/js/core.js new file mode 100644 index 0000000..b7918fb --- /dev/null +++ b/js/core.js @@ -0,0 +1,37 @@ +const API_BASE = window.API_BASE || localStorage.getItem('vyla.api') || ''; + +const K = '3f6703f3e2a0b2b40fe4fbb3e1546244', A = 'https://api.themoviedb.org/3', I = 'https://image.tmdb.org/t/p/'; +const $ = s => document.querySelector(s); +const main = $('#main'), rowsEl = $('#rows'), side = $('#side'), ov = $('#ov'), bgb = $('#bg'), hb = $('#hb'), sr = $('#sr'), sres = $('#sres'), chips = $('#chips'), tabs = $('#tabs'), kb = $('#kb'), pl = $('#pl'), vd = $('#pv'); +const api = async (p, q = '') => { try { const r = await fetch(`${A}${p}?api_key=${K}&language=en-US${q}`); return r.ok ? await r.json() : {} } catch (e) { return {} } }; +const ic = p => `${p}`; +const P_WL = '', P_FV = ''; +const LN = { wl: 'Watch Later', fav: 'Favorites' }; +const NAVEL = {}; let navAct = null, mode = '', gen = 0, sgen = 0, st; +let fast = false, G = {}, MG = [], TG = [], cur = null, lastMain = null, heroIt = null, idx = 0, busy = false, ht; +const LC = {}; + +function xf(box, url, cls = 'xf') { + if (box._u === url) return; box._u = url; + const im = new Image(); + im.onload = () => { if (box._u !== url) return; const d = document.createElement('div'); d.className = cls; d.style.backgroundImage = `url(${url})`; box.appendChild(d); requestAnimationFrame(() => requestAnimationFrame(() => d.classList.add('in'))); setTimeout(() => { while (box.children.length > 2) box.firstChild.remove() }, 800) }; + im.onerror = () => { if (box._u === url) box._u = null }; + im.src = url +} + +async function logo(it) { + const k = it._t + it.id; if (k in LC) return LC[k]; + const d = await api(`/${it._t}/${it.id}/images`, '&include_image_language=en,null'); + const l = (d.logos || []).find(x => x.iso_639_1 === 'en') || (d.logos || [])[0]; + return LC[k] = l ? I + 'w500' + l.file_path : null +} + +const yr = it => ((it.release_date || it.first_air_date || '') + '').slice(0, 4); +const gn = it => (it.genre_ids || []).map(i => G[i]).filter(Boolean).slice(0, 2).join(', '); + +const esc = t => String(t).replace(/ { const e = document.createElement('div'); e.className = cls + ' f'; e.tabIndex = -1; e.innerHTML = html; e._a = fn; return e }; +const LOGO = 'assets/favicon.ico', PH = 'data:image/svg+xml,' + encodeURIComponent(''); +function toast(m) { const t = document.createElement('div'); t.className = 'toast'; t.textContent = m; (document.fullscreenElement || document.body).appendChild(t); setTimeout(() => t.remove(), 2300) } +const imgDone = e => { const t = e.target; if (t.tagName === 'IMG') { t.classList.add('ld'); t.parentElement && t.parentElement.classList.add('lded') } }; +document.addEventListener('load', imgDone, true); document.addEventListener('error', imgDone, true); diff --git a/js/detail.js b/js/detail.js new file mode 100644 index 0000000..9d95c43 --- /dev/null +++ b/js/detail.js @@ -0,0 +1,66 @@ +let dgen = 0; +async function openDetail(it) { + const t = it._t; + if (!ov.classList.contains('show')) ov._prev = cur; + ov.classList.add('show'); side.classList.remove('open'); const my = ++dgen; ov.innerHTML = SK.detail(it); ov.scrollTop = 0; + const d = await api(`/${t}/${it.id}`, `&append_to_response=videos,credits,recommendations,images,${t === 'movie' ? 'release_dates' : 'content_ratings'}&include_image_language=en,null`); + if (my !== dgen || !ov.classList.contains('show')) return; + if (!d.id) { closeDetail(); return } + const title = d.title || d.name, year = ((d.release_date || d.first_air_date) || '').slice(0, 4); + const cert = t === 'movie' ? (((d.release_dates || {}).results || []).find(r => r.iso_3166_1 === 'US') || { release_dates: [] }).release_dates.map(x => x.certification).find(Boolean) : (((d.content_ratings || {}).results || []).find(r => r.iso_3166_1 === 'US') || {}).rating; + const rt = t === 'movie' ? (d.runtime ? `${Math.floor(d.runtime / 60)}h ${d.runtime % 60}m` : '') : (d.number_of_seasons ? `${d.number_of_seasons} Season${d.number_of_seasons > 1 ? 's' : ''}` : ''); + const lg = ((d.images || {}).logos || []).find(x => x.iso_639_1 === 'en') || ((d.images || {}).logos || [])[0]; + const bp = d.backdrop_path || d.poster_path; + ov.innerHTML = `
${(d.vote_average || 0).toFixed(1)}${year}${cert ? `${cert}` : ''}${rt}${(d.genres || []).slice(0, 3).map(g => g.name).join(', ')}
⋮

${d.overview || ''}

MORE
`; + const secs = $('#dsecs'); + const rowOf = (name, kids) => { if (!kids.length) return; const s = document.createElement('section'); s.className = 'ds'; s.innerHTML = `

${name}

`; s.lastChild.append(...kids); secs.appendChild(s) }; + const el = (c, h) => { const e = document.createElement('div'); e.className = c + ' f'; e.tabIndex = -1; e.innerHTML = h; e._a = () => { }; return e }; + if (t === 'tv') { + const ss = (d.seasons || []).filter(x => x.season_number > 0 && x.episode_count); + if (ss.length) { + const sec = document.createElement('section'); sec.className = 'ds eps'; + sec.innerHTML = '

Episodes

'; + secs.appendChild(sec); + const ddb = $('#ddb'), ddl = $('#ddl'), list = $('#el'); let cs = null; + const nm = x => x.name || 'Season ' + x.season_number; + const loadEps = async x => { + cs = x; ddb.innerHTML = `${esc(nm(x))}` + ic(''); + [...ddl.children].forEach((c, i) => c.classList.toggle('cur', ss[i] === x)); + list.innerHTML = SK.eps(10); + const r = await api(`/tv/${it.id}/season/${x.season_number}`); + if (cs !== x || !list.isConnected) return; + list.innerHTML = ''; + (r.episodes || []).forEach((p, ix) => { + const n = p.episode_number, img = p.still_path ? I + 'w300' + p.still_path : (it.backdrop_path ? I + 'w300' + it.backdrop_path : PH); + const e = el('ep', `
${esc(p.name || 'Episode ' + n)}${['Episode ' + n, p.runtime ? p.runtime + 'm' : ''].filter(Boolean).join(' \u2022 ')}`); + e.title = p.overview || ''; + e._a = () => openPlayer(it, `S${x.season_number} E${n} \u2022 ${p.name || ''}`, { it, seasons: ss, s: x.season_number, eps: r.episodes, i: ix, cache: { [x.season_number]: r.episodes } }); list.appendChild(e) + }); + const f0 = list.firstChild; if (f0 && f0._a && $('#dplay')) $('#dplay')._a = () => f0._a(); + if (!list.children.length) list.innerHTML = '

No episodes listed.

' + }; + ss.forEach(x => ddl.appendChild(mkf('ddi', `${esc(nm(x))}${x.episode_count} eps`, () => { closeDD(); loadEps(x) }))); + ddb._a = () => { if (!closeDD()) { ddl.classList.add('open'); setFocus(ddl.children[Math.max(0, ss.indexOf(cs))]) } }; + loadEps(ss[0]) + } + } + rowOf('Trailers', ((d.videos || {}).results || []).filter(v => v.site === 'YouTube' && /Trailer|Teaser|Clip/.test(v.type)).sort((a, b) => (b.type === 'Trailer') - (a.type === 'Trailer')).slice(0, 8).map(v => el('tr', `
${v.name}▶ ${v.type}`))); + rowOf('Related', ((d.recommendations || {}).results || []).filter(x => x.poster_path).map(x => { x._t = t; return card(x, false) })); + rowOf('Cast', ((d.credits || {}).cast || []).slice(0, 20).map(p => el('cs', `
${p.profile_path ? '' : p.name[0]}
${p.name}${p.character || ''}`))); + $('#dplay')._a = () => openPlayer(it); const sync = () => [['dwl', 'wl', P_WL], ['dfv', 'fav', P_FV]].forEach(([id, k, p]) => { const e = $('#' + id), v = has(k, it); e.classList.toggle('sv', v); e.dataset.l = (v ? 'Remove from ' : 'Add to ') + LN[k]; e.innerHTML = ic(p) }); + sync(); + $('#dmenu')._a = () => { const o = $('#dmx').classList.toggle('open'); $('#dmenu').classList.toggle('sv', o) }; + $('#dwl')._a = () => { toggleLib('wl', it); sync() }; $('#dfv')._a = () => { toggleLib('fav', it); sync() }; + $('#dmore')._a = () => { const o = $('#otx'), m = $('#dmore'); o.classList.toggle('x'); m.textContent = o.classList.contains('x') ? 'LESS' : 'MORE' }; + ov.scrollTop = 0; + setFocus($('#dplay')) +} + +function closeDetail() { + ov.classList.remove('show'); + const p = ov._prev; + setTimeout(() => { if (!ov.classList.contains('show')) ov.innerHTML = '' }, 400); + if (p && document.body.contains(p)) setFocus(p); else if (lib.classList.contains('show')) setFocus($('#lgrid .card') || navAct) +} + +function closeDD() { const l = $('#ddl'); if (l && l.classList.contains('open')) { l.classList.remove('open'); setFocus($('#ddb')); return true } return false } \ No newline at end of file diff --git a/js/focus.js b/js/focus.js new file mode 100644 index 0000000..2ba76a9 --- /dev/null +++ b/js/focus.js @@ -0,0 +1,60 @@ +function setFocus(el) { + if (!el) return; + const od = $('#ddl.open'); if (od && !el.closest('.dd')) od.classList.remove('open'); + cur && cur.classList.remove('on'); cur = el; el.classList.add('on'); + const inSide = !!el.closest('#side'), inOv = !!el.closest('#ov'); + side.classList.toggle('open', inSide && !ov.classList.contains('show')); + if (!inSide && !inOv && !el.closest('#pl')) lastMain = el; + const row = el.closest('.row'); + if (row) row.scrollTo({ left: Math.max(0, el.offsetLeft - parseInt(getComputedStyle(row).paddingLeft)), behavior: fast ? 'auto' : 'smooth' }); + + const menu = el.closest('#pm'); + if (menu) { + el.scrollIntoView({ block: 'nearest', behavior: fast ? 'auto' : 'smooth' }); + return; + } + + const scr = el.closest('#ov,#sr,#lib'); + if (scr && el.closest('#kb,#chips,#tabs')) scr.scrollTo({ top: 0, behavior: fast ? 'auto' : 'smooth' }); + else if (scr) { + const r = el.getBoundingClientRect(); + if (r.bottom > innerHeight - 50) scr.scrollBy({ top: r.bottom - innerHeight + 110, behavior: fast ? 'auto' : 'smooth' }); + else if (r.top < 50) scr.scrollBy({ top: r.top - 110, behavior: fast ? 'auto' : 'smooth' }); + } + else if (!inSide && !el.closest('#pl')) { + const sec = el.closest('.sec'); + main.scrollTo({ top: sec ? Math.max(0, sec.offsetTop - innerHeight * .52) : 0, behavior: fast ? 'auto' : 'smooth' }); + } + if (el._it) hero(el._it); if (el._bg) sbgf(el._bg); +} + +function nav(d) { + const sc = pl.classList.contains('show') ? (pl.classList.contains('eps') ? '#pep .f' : '#pl .f') : ov.classList.contains('show') ? '#ov .f' : sr.classList.contains('show') ? '#sr .f,#side .f' : lib.classList.contains('show') ? '#lib .f,#side .f' : '#side .f,#main .f'; + const c = cur.getBoundingClientRect(), cx = c.left + c.width / 2, cy = c.top + c.height / 2; + const inSide = !!cur.closest('#side'); + if (inSide && d === 'r' && lastMain && getComputedStyle(lastMain).visibility !== 'hidden') { setFocus(lastMain); return } + if ((d === 'l' || d === 'r') && !inSide) { + const g = cur.closest('.row,#kb,#tabs'); + if (g) { + const L = [...g.children].filter(e => e.classList.contains('f') && e.getBoundingClientRect().width), i = L.indexOf(cur), n = L[i + (d === 'r' ? 1 : -1)]; + if (i >= 0) { if (n) setFocus(n); else if (d === 'l' && !cur.closest('#ov,#pl')) setFocus(navAct); return } + } + } + let best = null, bs = 1e9; + document.querySelectorAll(sc).forEach(e => { + if (e === cur) return; + const r = e.getBoundingClientRect(); if (!r.width) return; + const x = r.left + r.width / 2, y = r.top + r.height / 2, dx = x - cx, dy = y - cy; + let p, s; + if (d === 'r') { if (dx < 8) return; p = dx; s = Math.abs(dy) * 2.5 } + else if (d === 'l') { if (dx > -8) return; p = -dx; s = Math.abs(dy) * 2.5 } + else if (d === 'd') { if (dy < 8) return; p = dy; s = Math.abs(dx) * 2 } + else { if (dy > -8) return; p = -dy; s = Math.abs(dx) * 2 } + if (p + s < bs) { bs = p + s; best = e } + }); + if (!best) return; + if (best.closest('#side') && !inSide) best = navAct; + setFocus(best) +} + +function act(el) { el && el._a && el._a() } \ No newline at end of file diff --git a/js/input.js b/js/input.js new file mode 100644 index 0000000..fb95ac5 --- /dev/null +++ b/js/input.js @@ -0,0 +1,99 @@ +const KM = { + 37: 'l', 21: 'l', + 38: 'u', 19: 'u', + 39: 'r', 22: 'r', + 40: 'd', 20: 'd', + 13: 'e', 23: 'e', 66: 'e', + 27: 'b', 8: 'b', 4: 'b', 10009: 'b', 461: 'b' +}; + +document.addEventListener('keydown', e => { + fast = e.repeat; + document.body.classList.add('kbm'); + + if (e.keyCode === 85 || e.keyCode === 179) { + e.preventDefault(); + tgl(); + return; + } + if (e.keyCode === 126 || e.keyCode === 250) { + e.preventDefault(); + vd.play(); + return; + } + if (e.keyCode === 127) { + e.preventDefault(); + vd.pause(); + return; + } + if (e.keyCode === 89 || e.keyCode === 227) { + e.preventDefault(); + vd.currentTime = Math.max(0, vd.currentTime - 10); + return; + } + if (e.keyCode === 90 || e.keyCode === 228) { + e.preventDefault(); + vd.currentTime = Math.min(vd.duration || Infinity, vd.currentTime + 10); + return; + } + + if (pl.classList.contains('show')) { + showCtl(); + if (e.key === ' ' || e.keyCode === 32) { e.preventDefault(); tgl(); return } + if (cur === $('#pseek') && (e.keyCode === 37 || e.keyCode === 39 || e.keyCode === 21 || e.keyCode === 22)) { + e.preventDefault(); + vd.currentTime += (e.keyCode === 39 || e.keyCode === 22) ? 10 : -10; + return; + } + if (cur && cur.id === 'vsl' && (e.keyCode === 37 || e.keyCode === 39 || e.keyCode === 21 || e.keyCode === 22)) { + e.preventDefault(); + vd.muted = false; + vd.volume = Math.min(1, Math.max(0, vd.volume + ((e.keyCode === 39 || e.keyCode === 22) ? .1 : -.1))); + cur._v(); + return; + } + } + + if (sr.classList.contains('show') && !ov.classList.contains('show') && !pl.classList.contains('show')) { + if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); setQ(sqv + e.key); return } + if (e.keyCode === 8) { e.preventDefault(); setQ(sqv.slice(0, -1)); return } + } + + const k = KM[e.keyCode]; + if (!k) return; + e.preventDefault(); + + if (!cur) { + const fallback = document.querySelector('.f'); + if (fallback) setFocus(fallback); + return; + } + + if (k === 'e') act(cur); + else if (k === 'b') { + if (pl.classList.contains('show')) { + pmK ? closeMenu(1) : pl.classList.contains('eps') ? closeEps(1) : closePlayer(); + } else if (ov.classList.contains('show')) { + closeDD() || closeDetail(); + } else if (lib.classList.contains('show')) { + closeLib(); + } else if (sr.classList.contains('show')) { + closeSearch(); + } + } else { + nav(k); + } +}); + +let mx = -1, my = -1; +document.addEventListener('mousemove', e => { + if (Math.abs(e.clientX - mx) + Math.abs(e.clientY - my) < 4) return; + mx = e.clientX; my = e.clientY; document.body.classList.remove('kbm'); + if (pl.classList.contains('show')) { showCtl(); const g = e.target.closest('.f'); if (g && g !== cur && pl.contains(g)) setFocus(g); return } + const f = e.target.closest('.f'); + if (f && f !== cur && (!ov.classList.contains('show') || ov.contains(f))) setFocus(f) +}); +document.addEventListener('click', e => { + const f = e.target.closest('.f'); if (f) act(f) +}); +ov.addEventListener('wheel', () => { }, { passive: true }); \ No newline at end of file diff --git a/js/library.js b/js/library.js new file mode 100644 index 0000000..5c9168c --- /dev/null +++ b/js/library.js @@ -0,0 +1,27 @@ +let LIB = { wl: [], fav: [] }, libK = null; const lib = $('#lib'); +try { Object.assign(LIB, JSON.parse(localStorage.getItem('vyla.lib') || '{}')) } catch (e) { } +const saveLib = () => { try { localStorage.setItem('vyla.lib', JSON.stringify(LIB)) } catch (e) { } }; +const slim = it => ({ id: it.id, _t: it._t || it.media_type, title: it.title, name: it.name, poster_path: it.poster_path, backdrop_path: it.backdrop_path, release_date: it.release_date, first_air_date: it.first_air_date, vote_average: it.vote_average, overview: it.overview }); +const has = (k, it) => LIB[k].some(x => x.id === it.id && x._t === it._t); +function toggleLib(k, it) { + const i = LIB[k].findIndex(x => x.id === it.id && x._t === it._t), had = i >= 0; + if (had) LIB[k].splice(i, 1); else LIB[k].unshift(slim(it)); + saveLib(); syncNav(); toast((had ? 'Removed from ' : 'Added to ') + LN[k]); + if (lib.classList.contains('show')) drawLib() +} +function drawLib() { + const k = libK, a = LIB[k], g = $('#lgrid'); + $('#lt').innerHTML = `${LN[k]}${a.length} title${a.length === 1 ? '' : 's'}`; + g.innerHTML = ''; + if (!a.length) { g.innerHTML = '

Nothing here yet. Open a title, press the \u22EE button and add it.

'; return } + a.forEach(x => { const c = card(x, false); c._bg = x; g.appendChild(c) }) +} +function openLib(k) { + libK = k; setAct(k); document.body.classList.add('lib'); lib.classList.add('show'); side.classList.remove('open'); lib.scrollTop = 0; + drawLib(); syncNav(); setFocus($('#lgrid .card') || NAVEL[k]) +} +function closeLib(silent) { + if (!lib.classList.contains('show')) return; + lib.classList.remove('show'); document.body.classList.remove('lib'); libK = null; heroIt = null; bgb._u = null; syncNav(); + if (!silent) { setAct(mode); setFocus($('#rows .card')) } +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..ef547ec --- /dev/null +++ b/js/main.js @@ -0,0 +1,8 @@ +(async () => { + rowsEl.innerHTML = SK.rows(4); + const [m, t] = await Promise.all([api('/genre/movie/list'), api('/genre/tv/list')]); + MG = m.genres || []; TG = t.genres || []; + [...MG, ...TG].forEach(g => G[g.id] = g.name); + if (!MG.length || !TG.length) { rowsEl.innerHTML = '

Could not reach TMDB.

'; return } + await go('discover'); +})(); diff --git a/js/player.js b/js/player.js new file mode 100644 index 0000000..e349f81 --- /dev/null +++ b/js/player.js @@ -0,0 +1,805 @@ +let currentHls = null; +let currentBgHls = null; +let currentSources = []; +let resolvedStreams = []; +let selectedSourceKey = null; +let selectedStreamUrl = null; +let expandedSources = new Set(); +let currentQualityLevels = []; +let selectedQualityIndex = -1; +let activeSubtitles = []; +let mediaSegments = []; +let activeSegment = null; +let plPrev = null; +let plT = null; +let plItem = null; +let pmK = null; +let plCtx = null; +let viewS = null; +let drag = 0; +let discoverySession = 0; +let userHasManuallySelectedSource = false; + +let subSettings = { + size: 24, + color: '#ffffff', + bg: 'rgba(0,0,0,0.75)', + offset: 0 +}; + +const pi = (p, f) => `${p}`; +const SPK = ''; +const PI = { + x: pi(''), + play: pi('', 1), + pause: pi('', 1), + cc: pi(''), + vol: pi(SPK + ''), + mute: pi(SPK + ''), + qlt: pi(''), + set: pi(''), + more: pi(''), + fs: pi(''), + fx: pi(''), + eps: pi(''), + chk: pi('') +}; + +$('#ptop').innerHTML = `
${PI.x}
${PI.qlt}
${PI.cc}
${PI.vol}
${PI.set}
${PI.more}
`; +$('#pbot').innerHTML = `0:00
0:00
${PI.eps}
${PI.fs}
`; +$('#pmid').innerHTML = PI.play; + +const pm = $('#pm'); +const sk = $('#pseek'); +const pbg = $('#pbg'); + +const fmt = t => { + t = Math.max(0, Math.floor(t || 0)); + const h = Math.floor(t / 3600); + const m = Math.floor((t % 3600) / 60); + const c = t % 60; + return (h ? h + ':' + String(m).padStart(2, '0') : m) + ':' + String(c).padStart(2, '0'); +}; + +const tgl = () => vd.paused ? vd.play() : vd.pause(); +const pico = () => { + $('#pmid').innerHTML = vd.paused ? PI.play : PI.pause; +}; + +function showCtl() { + pl.classList.add('ui'); + clearTimeout(plT); + plT = setTimeout(() => { + if (!vd.paused && !pmK && !pl.classList.contains('eps')) { + pl.classList.remove('ui'); + } + }, 3800); +} + +function closeMenu(rf) { + if (!pmK) return; + const t = { + qlt: '#pqlt', + cc: '#pcc', + vol: '#pvol', + set: '#pset', + more: '#pmore' + }[pmK]; + pmK = null; + pm.classList.remove('open'); + pm.innerHTML = ''; + if (rf) setFocus($(t)); +} + +function updateSubStyles() { + vd.classList.add('sub-active'); + vd.style.setProperty('--sub-size', `${subSettings.size}px`); + vd.style.setProperty('--sub-color', subSettings.color); + vd.style.setProperty('--sub-bg', subSettings.bg); + Array.from(vd.textTracks).forEach(track => { + if (track.cues) { + for (let i = 0; i < track.cues.length; i++) { + const cue = track.cues[i]; + if (cue && !cue._initialOffsetApplied) { + cue.startTime += subSettings.offset; + cue.endTime += subSettings.offset; + } + } + } + }); +} + +function applySubOffset(delta) { + subSettings.offset += delta; + Array.from(vd.textTracks).forEach(track => { + if (track.cues) { + for (let i = 0; i < track.cues.length; i++) { + const cue = track.cues[i]; + if (cue) { + cue.startTime += delta; + cue.endTime += delta; + } + } + } + }); +} + +function buildProxyUrl(rawUrl, headersObj = {}) { + return `${API_BASE}/api/proxy?url=${encodeURIComponent(rawUrl)}&headers=${encodeURIComponent(JSON.stringify(headersObj))}`; +} + +async function fetchSources() { + try { + const res = await fetch(`${API_BASE}/api/sources`); + const data = await res.json(); + currentSources = data.sources || []; + } catch (_) { + currentSources = []; + } +} + +async function startConcurrentDiscovery(it, s = null, e = null) { + const session = ++discoverySession; + pl.classList.add('buf'); + + resolvedStreams = []; + selectedStreamUrl = null; + let foundFirstPlayable = false; + + if (!currentSources.length) { + await fetchSources(); + } + + if (session !== discoverySession) return; + + const sourcePromises = currentSources.map(async (src) => { + let epQuery = s && e ? `&season=${s}&episode=${e}` : ''; + try { + const res = await fetch(`${API_BASE}/api/stream?provider=${src.key}&tmdbId=${it.id}${epQuery}`); + const data = await res.json(); + if (session !== discoverySession) return; + + if (data.success && data.stream) { + const streamData = data.stream; + const headers = streamData.headers || {}; + let playable = []; + + if (Array.isArray(streamData.allUrls) && streamData.allUrls.length > 0) { + playable = streamData.allUrls; + } else if (streamData.url) { + playable = [{ url: streamData.url, quality: streamData.quality || 'Auto', isM3U8: streamData.isM3U8 }]; + } + + if (playable.length > 0) { + const resolvedEntry = { + provider: src.key, + label: src.label || src.key, + headers, + primaryUrl: playable[0].url, + allUrls: playable + }; + + resolvedStreams.push(resolvedEntry); + + if (pmK === 'set') { + renderSourcesMenu(); + } + + if (!foundFirstPlayable && !userHasManuallySelectedSource) { + foundFirstPlayable = true; + selectedSourceKey = src.key; + playSpecificStream(resolvedEntry.primaryUrl, resolvedEntry.headers); + } else if (userHasManuallySelectedSource && selectedSourceKey === src.key && !selectedStreamUrl) { + playSpecificStream(resolvedEntry.primaryUrl, resolvedEntry.headers); + } + } + } + } catch (_) { } + }); + + Promise.allSettled(sourcePromises).then(() => { + if (session !== discoverySession) return; + if (!resolvedStreams.length) { + pl.classList.remove('buf'); + toast('No playable sources available.'); + } + }); + + loadSubtitles(it.id, s, e); + loadTimestamps(it.id, s, e); +} + +function playSpecificStream(streamUrl, headers = {}) { + pl.classList.add('buf'); + selectedStreamUrl = streamUrl; + + if (currentHls) { + currentHls.destroy(); + currentHls = null; + } + if (currentBgHls) { + currentBgHls.destroy(); + currentBgHls = null; + } + + const finalUrl = buildProxyUrl(streamUrl, headers); + + if (streamUrl.includes('.m3u8')) { + if (Hls.isSupported()) { + currentHls = new Hls({ + xhrSetup: function (xhr) { + xhr.withCredentials = false; + } + }); + currentHls.loadSource(finalUrl); + currentHls.attachMedia(vd); + currentHls.on(Hls.Events.MANIFEST_PARSED, (_, data) => { + currentQualityLevels = data.levels || []; + vd.play().catch(() => { }); + }); + currentHls.on(Hls.Events.LEVEL_SWITCHED, (_, data) => { + selectedQualityIndex = data.level; + }); + + if (pbg) { + currentBgHls = new Hls({ + xhrSetup: function (xhr) { + xhr.withCredentials = false; + } + }); + currentBgHls.loadSource(finalUrl); + currentBgHls.attachMedia(pbg); + currentBgHls.on(Hls.Events.MANIFEST_PARSED, () => { + pbg.muted = true; + if (!vd.paused) pbg.play().catch(() => { }); + }); + } + } else if (vd.canPlayType('application/vnd.apple.mpegurl')) { + vd.src = finalUrl; + vd.play().catch(() => { }); + if (pbg) { + pbg.src = finalUrl; + pbg.muted = true; + pbg.play().catch(() => { }); + } + } + } else { + currentQualityLevels = []; + vd.src = finalUrl; + vd.play().catch(() => { }); + if (pbg) { + pbg.src = finalUrl; + pbg.muted = true; + pbg.play().catch(() => { }); + } + } +} + +function setPlaybackQuality(levelIndex) { + selectedQualityIndex = levelIndex; + if (currentHls) { + currentHls.currentLevel = levelIndex; + } +} + +async function loadSubtitles(id, s, e) { + Array.from(vd.querySelectorAll('track')).forEach(t => t.remove()); + activeSubtitles = []; + + try { + let epQuery = s && e ? `&season=${s}&episode=${e}` : ''; + const res = await fetch(`${API_BASE}/api/subtitles?tmdbId=${id}${epQuery}`); + const data = await res.json(); + activeSubtitles = data.subtitles || []; + + activeSubtitles.forEach((sub, idx) => { + const track = document.createElement('track'); + track.kind = 'subtitles'; + track.label = `${sub.label} (${sub.source || 'mirror'})`; + track.srclang = sub.label.slice(0, 2).toLowerCase(); + track.src = buildProxyUrl(sub.file); + track.default = idx === 0; + vd.appendChild(track); + track.track.mode = 'disabled'; + }); + updateSubStyles(); + } catch (_) { } +} + +async function loadTimestamps(tmdbId, season = null, episode = null, durationSec = null) { + mediaSegments = []; + activeSegment = null; + $('#pskip').classList.remove('v'); + try { + let q = `${API_BASE}/api/timestamps?tmdb_id=${tmdbId}`; + if (season) q += `&season=${season}`; + if (episode) q += `&episode=${episode}`; + if (durationSec) q += `&duration_ms=${Math.round(durationSec * 1000)}`; + + const res = await fetch(q); + const data = await res.json(); + mediaSegments = data.segments || []; + } catch (_) { + mediaSegments = []; + } +} + +function renderSourcesMenu() { + let container = $('#pm-src-list'); + if (!container) return; + container.innerHTML = ''; + + if (!resolvedStreams.length) { + container.innerHTML = '
Probing providers...
'; + return; + } + + resolvedStreams.forEach(s => { + const grp = document.createElement('div'); + grp.className = 'src-group'; + + const hdr = document.createElement('div'); + hdr.className = 'src-header f' + (s.provider === selectedSourceKey ? ' on' : ''); + + const hasMulti = s.allUrls && s.allUrls.length > 1; + const countLabel = hasMulti ? ` (${s.allUrls.length})` : ''; + const expandIcon = hasMulti ? `${expandedSources.has(s.provider) ? '▲' : '▼'}` : ''; + + hdr.innerHTML = `${s.label}${countLabel}
${expandIcon}${s.provider === selectedSourceKey ? PI.chk : ''}
`; + hdr._a = () => { + userHasManuallySelectedSource = true; + if (hasMulti && s.provider === selectedSourceKey) { + if (expandedSources.has(s.provider)) expandedSources.delete(s.provider); + else expandedSources.add(s.provider); + renderSourcesMenu(); + } else { + selectedSourceKey = s.provider; + playSpecificStream(s.primaryUrl, s.headers); + renderSourcesMenu(); + } + }; + grp.appendChild(hdr); + + if (hasMulti) { + const sub = document.createElement('div'); + sub.className = 'src-sublist' + (expandedSources.has(s.provider) ? ' open' : ''); + + s.allUrls.forEach((u, i) => { + const item = document.createElement('div'); + item.className = 'src-subitem f' + (u.url === selectedStreamUrl ? ' on' : ''); + item.innerHTML = `${u.quality || 'Stream ' + (i + 1)}${u.url === selectedStreamUrl ? PI.chk : ''}`; + item._a = () => { + userHasManuallySelectedSource = true; + selectedSourceKey = s.provider; + playSpecificStream(u.url, s.headers); + renderSourcesMenu(); + }; + sub.appendChild(item); + }); + grp.appendChild(sub); + } + + container.appendChild(grp); + }); +} + +function openMenu(k) { + if (pmK === k) { + closeMenu(1); + return; + } + closeEps(); + pm.innerHTML = ''; + pmK = k; + pm.classList.add('open'); + let first = null; + + const h = t => { + const e = document.createElement('h4'); + e.textContent = t; + pm.appendChild(e); + }; + + const r = (l, on, fn) => { + const e = mkf('pi', `${l}${on ? PI.chk : ''}`, () => { + fn(); + closeMenu(1); + }); + pm.appendChild(e); + if (on && !first) first = e; + return e; + }; + + if (k === 'qlt') { + h('Stream Quality'); + if (currentHls && currentQualityLevels.length > 0) { + r('Auto', selectedQualityIndex === -1, () => setPlaybackQuality(-1)); + currentQualityLevels.forEach((lvl, idx) => { + const label = lvl.height ? `${lvl.height}p` : `${Math.round(lvl.bitrate / 1000)} kbps`; + r(label, selectedQualityIndex === idx, () => setPlaybackQuality(idx)); + }); + } else { + const activeProv = resolvedStreams.find(s => s.provider === selectedSourceKey); + if (activeProv && activeProv.allUrls.length > 1) { + activeProv.allUrls.forEach((u, idx) => { + const label = u.quality || `Stream ${idx + 1}`; + r(label, u.url === selectedStreamUrl, () => { + userHasManuallySelectedSource = true; + playSpecificStream(u.url, activeProv.headers); + }); + }); + } else { + r('Auto', true, () => { }); + } + } + } else if (k === 'cc') { + h('Subtitles'); + const T = [...vd.textTracks]; + r('Off', !T.some(t => t.mode === 'showing'), () => { + T.forEach(t => t.mode = 'disabled'); + }); + T.forEach(t => r(t.label, t.mode === 'showing', () => { + T.forEach(u => u.mode = u === t ? 'showing' : 'disabled'); + updateSubStyles(); + })); + + h('Subtitle Size'); + [18, 22, 26, 32].forEach(sz => { + r(`${sz}px`, subSettings.size === sz, () => { + subSettings.size = sz; + updateSubStyles(); + }); + }); + + h('Subtitle Color'); + [['White', '#ffffff'], ['Yellow', '#ffd700'], ['Cyan', '#00ffff']].forEach(([col, code]) => { + r(col, subSettings.color === code, () => { + subSettings.color = code; + updateSubStyles(); + }); + }); + + h('Subtitle Sync'); + const syncRow = document.createElement('div'); + syncRow.className = 'sub-ctrl-row'; + syncRow.innerHTML = `Timing Offset
`; + pm.appendChild(syncRow); + $('#sub-min').onclick = (ev) => { + ev.stopPropagation(); + applySubOffset(-0.5); + }; + $('#sub-plus').onclick = (ev) => { + ev.stopPropagation(); + applySubOffset(0.5); + }; + } else if (k === 'vol') { + h('Volume'); + const w = document.createElement('div'); + w.className = 'vrow'; + w.innerHTML = '
'; + pm.appendChild(w); + const vm = $('#vm'); + const vs = $('#vsl'); + const vu = () => { + const v = vd.muted ? 0 : vd.volume; + vs.firstChild.style.width = v * 100 + '%'; + $('#vpc').textContent = Math.round(v * 100); + vm.innerHTML = $('#pvol').innerHTML = v ? PI.vol : PI.mute; + }; + vm._a = () => { + vd.muted = !vd.muted; + vu(); + }; + vs._a = () => { }; + vs._v = vu; + vs.addEventListener('click', e => { + const b = vs.getBoundingClientRect(); + vd.muted = false; + vd.volume = Math.min(1, Math.max(0, (e.clientX - b.left) / b.width)); + vu(); + }); + vu(); + first = vs; + } else if (k === 'set') { + h('Available Sources'); + const srcContainer = document.createElement('div'); + srcContainer.id = 'pm-src-list'; + pm.appendChild(srcContainer); + renderSourcesMenu(); + + h('Playback Speed'); + [.5, 1, 1.25, 1.5, 2].forEach(v => r(v === 1 ? 'Normal' : v + '\u00d7', vd.playbackRate === v, () => vd.playbackRate = v)); + + h('Aspect Ratio'); + r('Fit Screen', !pl.classList.contains('cover'), () => { + pl.classList.remove('cover'); + if (pbg) { + pbg.currentTime = vd.currentTime; + if (!vd.paused) pbg.play().catch(() => { }); + } + }); + r('Fill Screen', pl.classList.contains('cover'), () => { + pl.classList.add('cover'); + if (pbg) pbg.pause(); + }); + } else { + h('Library'); + r('Watch Later', has('wl', plItem), () => toggleLib('wl', plItem)); + r('Favorites', has('fav', plItem), () => toggleLib('fav', plItem)); + } + + showCtl(); + setFocus(first || pm.querySelector('.f')); +} + +async function openPlayer(it, ep, ctx) { + if (!it) return; + plPrev = cur; + plItem = it; + plCtx = ctx || null; + userHasManuallySelectedSource = false; + $('#peps').style.display = plCtx ? '' : 'none'; + pl.classList.remove('eps'); + $('#pnext').classList.remove('v'); + $('#pt').textContent = it.title || it.name || ''; + $('#ps').textContent = ep || [yr(it), it._t === 'tv' ? 'Series' : 'Movie'].filter(Boolean).join(' \u2022 '); + $('#pav').src = it.poster_path ? I + 'w185' + it.poster_path : PH; + vd.poster = it.backdrop_path ? I + 'w1280' + it.backdrop_path : ''; + + pl.classList.add('show'); + showCtl(); + pico(); + setFocus($('#pmid')); + + if (plCtx) { + const epData = plCtx.eps[plCtx.i]; + startConcurrentDiscovery(it, plCtx.s, epData.episode_number); + } else { + startConcurrentDiscovery(it); + } +} + +function closePlayer() { + closeMenu(); + closeEps(); + if (currentHls) { + currentHls.destroy(); + currentHls = null; + } + if (currentBgHls) { + currentBgHls.destroy(); + currentBgHls = null; + } + plCtx = null; + vd.pause(); + vd.removeAttribute('src'); + vd.load(); + if (pbg) { + pbg.pause(); + pbg.removeAttribute('src'); + pbg.load(); + } + if (document.fullscreenElement) document.exitFullscreen(); + pl.classList.remove('show', 'ui'); + if (plPrev && document.body.contains(plPrev)) setFocus(plPrev); +} + +$('#pback')._a = closePlayer; +$('#pmid')._a = tgl; + +$('#pskip')._a = () => { + if (activeSegment) { + const target = activeSegment.end != null ? activeSegment.end + 0.5 : (vd.duration || vd.currentTime + 30); + vd.currentTime = target; + $('#pskip').classList.remove('v'); + activeSegment = null; + } else { + vd.currentTime += 85; + } +}; + +$('#pqlt')._a = () => openMenu('qlt'); +$('#pcc')._a = () => openMenu('cc'); +$('#pvol')._a = () => openMenu('vol'); +$('#pset')._a = () => openMenu('set'); +$('#pmore')._a = () => openMenu('more'); +$('#pfs')._a = () => document.fullscreenElement ? document.exitFullscreen() : pl.requestFullscreen && pl.requestFullscreen(); + +document.addEventListener('fullscreenchange', () => { + $('#pfs').innerHTML = document.fullscreenElement ? PI.fx : PI.fs; +}); + +const sp = e => { + const b = sk.getBoundingClientRect(); + vd.currentTime = Math.min(1, Math.max(0, (e.clientX - b.left) / b.width)) * (vd.duration || 0); +}; + +sk.addEventListener('pointerdown', e => { + drag = 1; + sk._m = 1; + sk.setPointerCapture(e.pointerId); + sp(e); +}); + +sk.addEventListener('pointermove', e => { + if (drag) sp(e); +}); + +sk.addEventListener('pointerup', () => drag = 0); + +sk._a = () => { + if (sk._m) { + sk._m = 0; + return; + } + tgl(); +}; + +pl.querySelectorAll('.f').forEach(e => { + if (!e._a) e._a = () => { }; +}); + +vd.addEventListener('click', () => { + if (pmK) closeMenu(); + else if (pl.classList.contains('eps')) closeEps(); + else tgl(); + showCtl(); +}); + +vd.addEventListener('play', () => { + if (!pl.classList.contains('cover') && pbg) { + pbg.muted = true; + pbg.currentTime = vd.currentTime; + pbg.play().catch(() => { }); + } +}); + +vd.addEventListener('pause', () => { + if (pbg) pbg.pause(); +}); + +vd.addEventListener('seeking', () => { + if (pbg) pbg.currentTime = vd.currentTime; +}); + +vd.addEventListener('seeked', () => { + if (pbg) pbg.currentTime = vd.currentTime; +}); + +vd.addEventListener('ratechange', () => { + if (pbg) pbg.playbackRate = vd.playbackRate; +}); + +vd.addEventListener('durationchange', () => { + $('#pdur').textContent = fmt(vd.duration); + if (vd.duration && plItem) { + const c = plCtx; + loadTimestamps(plItem.id, c ? c.s : null, c ? c.eps[c.i].episode_number : null, vd.duration); + } +}); + +vd.addEventListener('timeupdate', () => { + const d = vd.duration || 0; + const t = vd.currentTime; + const p = d ? (t / d) * 100 : 0; + + $('#pfill').style.width = p + '%'; + $('#phd').style.left = p + '%'; + $('#pcur').textContent = fmt(t); + + if (d && vd.buffered.length) { + $('#pbuf').style.width = (vd.buffered.end(vd.buffered.length - 1) / d * 100) + '%'; + } + + if (!pl.classList.contains('cover') && pbg && Math.abs(pbg.currentTime - t) > 0.35) { + pbg.currentTime = t; + } + + const currentSeg = mediaSegments.find(seg => { + const afterStart = t >= seg.start; + const beforeEnd = seg.end == null ? (d ? t < d - 5 : true) : t < seg.end; + return afterStart && beforeEnd; + }); + + if (currentSeg) { + activeSegment = currentSeg; + $('#pskip').textContent = currentSeg.label; + $('#pskip').classList.add('v'); + } else { + activeSegment = null; + $('#pskip').classList.remove('v'); + } +}); + +['play', 'pause', 'ended'].forEach(n => vd.addEventListener(n, pico)); +['loadstart', 'waiting', 'seeking'].forEach(n => vd.addEventListener(n, () => pl.classList.add('buf'))); +['playing', 'canplay', 'seeked', 'error'].forEach(n => vd.addEventListener(n, () => pl.classList.remove('buf'))); + +const hasNext = () => !!plCtx && (plCtx.i + 1 < plCtx.eps.length || plCtx.seasons.some(x => x.season_number > plCtx.s)); +const getSeason = async s => plCtx.cache[s] || (plCtx.cache[s] = (await api(`/tv/${plCtx.it.id}/season/${s}`)).episodes || []); + +async function playEp(s, i) { + const c = plCtx; + if (!c) return; + const eps = await getSeason(s); + const p = eps[i]; + if (!p || plCtx !== c) return; + c.s = s; + c.i = i; + c.eps = eps; + $('#ps').textContent = `S${s} E${p.episode_number} \u2022 ${p.name || ''}`; + $('#pnext').classList.remove('v'); + closeEps(); + showCtl(); + setFocus($('#pmid')); + userHasManuallySelectedSource = false; + startConcurrentDiscovery(c.it, s, p.episode_number); +} + +function nextEp() { + const c = plCtx; + if (!hasNext()) return; + if (c.i + 1 < c.eps.length) { + playEp(c.s, c.i + 1); + } else { + playEp(c.seasons.find(x => x.season_number > c.s).season_number, 0); + } +} + +function closeEps(rf) { + if (!pl.classList.contains('eps')) return; + pl.classList.remove('eps'); + if (rf) setFocus($('#peps')); +} + +function openEps() { + const c = plCtx; + if (!c) return; + closeMenu(); + $('#peT').innerHTML = `Episodes${esc(c.it.title || c.it.name || '')}`; + const sr = $('#pseas'); + sr.innerHTML = ''; + c.seasons.forEach(x => { + sr.appendChild(mkf('chip', esc(x.name || 'Season ' + x.season_number), () => showSeason(x.season_number))); + }); + pl.classList.add('eps'); + showCtl(); + showSeason(c.s, true); +} + +async function showSeason(sn, focus) { + const c = plCtx; + const car = $('#pcar'); + if (!c) return; + viewS = sn; + [...$('#pseas').children].forEach((e, i) => e.classList.toggle('act', c.seasons[i].season_number === sn)); + car.innerHTML = SK.pep(8); + const eps = await getSeason(sn); + if (viewS !== sn || plCtx !== c) return; + car.innerHTML = ''; + eps.forEach((p, i) => { + const n = p.episode_number; + const cur = c.s === sn && c.i === i; + const img = p.still_path ? I + 'w300' + p.still_path : (c.it.backdrop_path ? I + 'w300' + c.it.backdrop_path : PH); + car.appendChild(mkf('pe' + (cur ? ' cur' : ''), `
${n}${cur ? '' : ''}
${esc(p.name || 'Episode ' + n)}${p.runtime ? p.runtime + 'm' : 'Episode ' + n}`, () => playEp(sn, i))); + }); + const t = car.querySelector('.cur') || car.firstChild; + if (focus && t) setFocus(t); +} + +$('#pep').innerHTML = `

${PI.x}
`; +$('#pnext').innerHTML = 'Next Episode' + pi(''); +$('#pnext')._a = nextEp; +$('#pepx')._a = () => closeEps(1); +$('#peps')._a = () => pl.classList.contains('eps') ? closeEps(1) : openEps(); + +vd.addEventListener('timeupdate', () => { + const d = vd.duration || 0; + const r = d - vd.currentTime; + $('#pnext').classList.toggle('v', !!d && hasNext() && r <= 300 && r > 0); + $('#pep').style.setProperty('--p', d ? (vd.currentTime / d) * 100 + '%' : '0%'); +}); + +vd.addEventListener('ended', () => { + if (hasNext()) nextEp(); +}); \ No newline at end of file diff --git a/js/search.js b/js/search.js new file mode 100644 index 0000000..2204daa --- /dev/null +++ b/js/search.js @@ -0,0 +1,70 @@ +let sqv = '', bgT, stab = 0, SD = { q: '', all: [], mv: [], tv: [] }; +const SVS = ''; +const sdraw = () => { const t = $('#stext'); t.textContent = sqv || 'Search'; t.classList.toggle('q', !!sqv) }; +function setQ(v) { sqv = v; sdraw(); clearTimeout(st); st = setTimeout(() => fillSearch(sqv.trim()), 300) } +function sbgf(it) { clearTimeout(bgT); bgT = setTimeout(() => xf(bgb, I + 'w780' + (it.backdrop_path || it.poster_path)), 120) } +async function fillSearch(q) { + const my = ++sgen; skelSearch(); + let all, mv, tv; + const ok = x => x.poster_path && (x.media_type === 'movie' || x.media_type === 'tv'); + if (q) { + const d = await api('/search/multi', `&query=${encodeURIComponent(q)}&include_adult=false`); + if (my !== sgen) return; + all = (d.results || []).filter(ok); mv = all.filter(x => x.media_type === 'movie'); tv = all.filter(x => x.media_type === 'tv') + } + else { + const [a, b] = await Promise.all([api('/trending/movie/week'), api('/trending/tv/week')]); + if (my !== sgen) return; + mv = (a.results || []).filter(x => x.poster_path).map(x => ({ ...x, media_type: 'movie' })); + tv = (b.results || []).filter(x => x.poster_path).map(x => ({ ...x, media_type: 'tv' })); all = [] + } + SD = { q, all, mv, tv }; drawSearch() +} +function tile(x) { + x._t = x.media_type; + const e = mkf('sc', `
${esc(x.title || x.name || '')}★ ${(x.vote_average || 0).toFixed(1)}${yr(x)}${x.media_type === 'tv' ? 'Show' : 'Movie'}`, () => openDetail(x)); + e._bg = x; return e +} +function drawSearch() { + const { q, all, mv, tv } = SD; + chips.innerHTML = ''; + const names = [...new Set((q ? all : [...mv, ...tv]).map(x => x.title || x.name))].slice(0, 9); + if (q) chips.appendChild(mkf('chip', SVS + `"${esc(q)}"`, () => { })); + names.forEach(n => chips.appendChild(mkf('chip', esc(n), () => setQ(n)))); + sres.innerHTML = ''; + const L = stab === 1 ? [['Movies', mv]] : stab === 2 ? [['TV Shows', tv]] : q ? [['Top Matches', all.slice(0, 10)], ['Movies', mv], ['TV Shows', tv]] : [['Trending Movies', mv], ['Trending TV Shows', tv]]; + let n = 0; + L.forEach(([h, a]) => { if (!a.length) return; n++; const sec = document.createElement('section'); sec.className = 'ss'; sec.innerHTML = `

${h}

`; sec.lastChild.append(...a.map(tile)); sres.appendChild(sec) }); + if (!n) sres.innerHTML = `

No results${q ? ` for \u201c${esc(q)}\u201d` : ''}

`; + [...tabs.children].forEach((t, i) => t.classList.toggle('act', i === stab)); + if (cur && !document.body.contains(cur)) setFocus(chips.children[1] || chips.firstChild || kb.children[2]) +} +['Top Results', 'Movies', 'TV Shows'].forEach((t, i) => tabs.appendChild(mkf('tab', t, () => { stab = i; drawSearch() }))); +const KL = 'abcdefghijklmnopqrstuvwxyz'.split(''), KN = "1234567890.-'&:".split(''); +let num = false; +function buildKb() { + kb.innerHTML = ''; + const mk = (label, cls, fn) => kb.appendChild(mkf('k ' + cls, label, () => { fn(); sr.scrollTo({ top: 0, behavior: 'smooth' }) })); + mk(num ? 'ABC' : '123', 'w', () => { num = !num; buildKb(); setFocus(kb.firstChild) }); + mk('SPACE', 'w', () => { if (sqv && !sqv.endsWith(' ')) setQ(sqv + ' ') }); + (num ? KN : KL).forEach(c => mk(c, '', () => setQ(sqv + c))); + mk('', 'd', () => setQ(sqv.slice(0, -1))) +} +buildKb(); +function openSearch() { + setAct('search'); document.body.classList.add('srch'); sr.classList.add('show'); side.classList.remove('open'); sr.scrollTop = 0; + if (!sres.children.length) fillSearch(''); + setFocus(kb.children[2]) +} +function closeSearch(silent) { + if (!sr.classList.contains('show')) return; + sr.classList.remove('show'); document.body.classList.remove('srch'); + heroIt = null; bgb._u = null; + if (!silent) { setAct(mode); setFocus(document.querySelector('#rows .card')) } +} + + +function skelSearch() { + chips.innerHTML = SK.chips(7); + sres.innerHTML = [0, 1].map(() => `
${SK.tiles(9)}
`).join('') +} diff --git a/js/sidebar.js b/js/sidebar.js new file mode 100644 index 0000000..edb0a85 --- /dev/null +++ b/js/sidebar.js @@ -0,0 +1,16 @@ +const NAV = [ + ['search', 'Search', ic('')], + ['discover', 'Discovery', ic('')], + ['movie', 'Movies', ic('')], + ['tv', 'TV Shows', ic('')], + ['wl', 'Watch Later', ic(P_WL)], + ['fav', 'Favorites', ic(P_FV)] +]; + +NAV.forEach(([k, n, s]) => { const e = document.createElement('div'); e.className = 'ni f'; e.tabIndex = -1; e.innerHTML = s + `${n}`; e._a = () => go(k); NAVEL[k] = e; $('#nav').appendChild(e) }); +syncNav(); +function setAct(k) { Object.entries(NAVEL).forEach(([n, e]) => e.classList.toggle('act', n === k)); navAct = NAVEL[k] } +const mark = $('#mark'); mark.onerror = () => { mark.onerror = null; mark.src = PH }; mark.src = LOGO; + + +function syncNav() { ['wl', 'fav'].forEach(k => { if (NAVEL[k]) NAVEL[k].style.display = (LIB[k].length || libK === k) ? '' : 'none' }) } diff --git a/js/skeleton.js b/js/skeleton.js new file mode 100644 index 0000000..7358c76 --- /dev/null +++ b/js/skeleton.js @@ -0,0 +1,13 @@ +const SK = { + cards: n => '
'.repeat(n), + tiles: n => '
'.repeat(n), + chips: n => Array.from({ length: n }, (_, i) => `
`).join(''), + pep: n => '
'.repeat(n), + eps: n => '
'.repeat(n), + rows: n => Array.from({ length: n }, () => '
' + SK.cards(14) + '
').join(''), + detail: it => { + const bp = it.backdrop_path || it.poster_path || ''; + return `
` + + `
${'
'.repeat(4)}
${SK.cards(9)}
` + } +}; \ No newline at end of file diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..5e9be4e --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "tv", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@vyla-entertainment/sdk": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@vyla-entertainment/sdk/-/sdk-1.14.0.tgz", + "integrity": "sha512-ZZm3xnfuWTDI0IU/UyGnPgFmSCFjPzIyb71EauggrFfRa6ZyjSrjNAAH8onbHyD5uO3TkJFUHF8VIeFJK8t/yA==", + "license": "CC-BY-NC-4.0" + } + } +} diff --git a/node_modules/@vyla-entertainment/sdk/.env.example b/node_modules/@vyla-entertainment/sdk/.env.example new file mode 100644 index 0000000..a643bc8 --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/.env.example @@ -0,0 +1 @@ +TMDB_API_KEY=your_tmdb_api_key_here \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/.github/workflows/publish.yml b/node_modules/@vyla-entertainment/sdk/.github/workflows/publish.yml new file mode 100644 index 0000000..8ac2f68 --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/.github/workflows/publish.yml @@ -0,0 +1,38 @@ +name: Publish npm package + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org/ + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - run: npm ci + + - name: Bump version + run: | + npm version minor + git push --follow-tags + + - name: Publish + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_KEY }} diff --git a/node_modules/@vyla-entertainment/sdk/.gitlab-ci.yml b/node_modules/@vyla-entertainment/sdk/.gitlab-ci.yml new file mode 100644 index 0000000..717d1e7 --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/.gitlab-ci.yml @@ -0,0 +1,16 @@ +stages: + - build + - test + - deploy + - publish + +publish: + stage: publish + image: node:22 + rules: + - if: '$CI_COMMIT_TAG' + when: manual + script: + - npm ci + - echo "//registry.npmjs.org/:_authToken=${NPM_KEY}" > .npmrc + - npm publish \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/LICENSE b/node_modules/@vyla-entertainment/sdk/LICENSE new file mode 100644 index 0000000..12d4ca7 --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/LICENSE @@ -0,0 +1,407 @@ +Attribution-NonCommercial 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + j. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + k. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + l. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/README.md b/node_modules/@vyla-entertainment/sdk/README.md new file mode 100644 index 0000000..5ef2f87 --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/README.md @@ -0,0 +1,31 @@ +# vyla/sdk +VylaSDK is a javascript base version of [Vyla](https://vyla.mintlify.app/)'s stream & scrape api, allowing developers to access content providers directly without using a REST api. This gives you the power & updates of Vyla without the headaches of api keys & server reliability. + +## Installing +To start, install VylaSDK through npm or pnpm: +```bash +# npm +npm install @vyla-entertainment/sdk + +# pnpm +pnpm install @vyla-entertainment/sdk +``` + +View our full documentation at https://docs.vyla.cc/sdk + +# [DMCA](https://docs.vyla.cc/misc/dmca) + +If your source is listed here, I sincerely apologize. I understand that its inclusion may be costing you a lot of money, and that was never my intention. Please contact me as soon as possible, and I'll remove it ASAP. + +This is a small hobby project that I built for learning and personal interest, not for profit. I genuinely want to respect the wishes of source owners and will address any removal requests as quickly as I can. + +This project is developed and maintained by [Vyla Entertainment](https://github.com/vyla-entertainment) (@vyla-entertainment). + +Original work and core authorship: + +[@alwaysmesmerizingyou](https://gitlab.com/alwaysmesmerizingyou) + +[@GavinGoGaming](https://github.com/GavinGoGaming) + +Full credits and acknowledgements: +https://docs.vyla.cc/misc/credit \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/package.json b/node_modules/@vyla-entertainment/sdk/package.json new file mode 100644 index 0000000..1db9b37 --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/package.json @@ -0,0 +1,33 @@ +{ + "name": "@vyla-entertainment/sdk", + "version": "1.14.0", + "scripts": { + "test": "node test/index.js" + }, + "author": "vyla-entertainment", + "contributors": [ + { + "name": "GavinGoGaming", + "email": "gavin@klash.dev" + }, + { + "name": "alwaysmesmerizingyou", + "email": "alwaysmesmerizingyou@gmail.com" + } + ], + "license": "CC-BY-NC-4.0", + "type": "module", + "description": "Vyla's multi-provider stream scraper SDK", + "main": "src/sdk.js", + "types": "src/sdk.d.ts", + "directories": { + "test": "test" + }, + "keywords": [], + "devDependencies": { + "dotenv": "^17.4.2" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/src/config.js b/node_modules/@vyla-entertainment/sdk/src/config.js new file mode 100644 index 0000000..e0223a9 --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/src/config.js @@ -0,0 +1,487 @@ +// Currently active sources reported on 6:18:22 PM PST 9/13/2026 + +// None Anime Tested: 936075 +// Anime Tested: 37854 + +// Fshare/Fsonic Tested: 155 ( they don't have Michael ) + +export const SOURCES = [ + { + key: '123anime', + label: '123Anime', + sourceFile: '123anime', + proxyParam: 'a1', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: false, + skipProxy: true, + cdnHeaders: [{ + pattern: /hlsx\d+cdn\.|burntburst\d+\.store|echovideo\.ru/i, + headers: { + Referer: 'https://play2.echovideo.ru/', + Origin: 'https://play2.echovideo.ru', + }, + },], + }, + + { + key: '111477', + label: '111477', + sourceFile: '111477', + proxyParam: 'a11', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /111477\.xyz/i, + headers: { + Referer: 'https://st.111477.xyz/', + Origin: 'https://st.111477.xyz', + }, + }], + }, + + { + key: 'anihq-sub', + label: 'AniHQ (Sub)', + sourceFile: 'anihq', + proxyParam: 'ahsub', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true + }, + + { + key: 'anihq-dub', + label: 'AniHQ (Dub)', + sourceFile: 'anihq', + proxyParam: 'ahdub', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true + }, + + { + key: 'anineko-sub', + sourceFile: 'anineko', + label: 'AniNeko (Sub)', + proxyParam: 'anksub', + timeout: 25000, + jitter: 500, + retries: 2 + }, + + { + key: 'anineko-dub', + sourceFile: 'anineko', + label: 'AniNeko (Dub)', + proxyParam: 'ankdub', + timeout: 25000, + jitter: 500, + retries: 2 + }, + + { + key: 'bcine', + label: 'Bcine', + sourceFile: 'bcine', + proxyParam: 'bc', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /1embed\.cc|videasy\.to/i, + headers: { + Referer: 'https://bcine.ru/', + Origin: 'https://bcine.ru', + }, + }], + }, + + { + key: 'dulo', + label: 'Dulo', + sourceFile: 'dulo', + proxyParam: 'dl', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /dulo\.mov/i, + headers: { + Referer: 'https://dulo.mov/', + Origin: 'https://dulo.mov', + }, + }], + }, + + { + key: 'flaxmovies', + label: 'FlaxMovies', + sourceFile: 'flaxmovies', + proxyParam: 'fx', + timeout: 20000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /flix2watch\.pro/i, + headers: { + Referer: 'https://flaxmovies.xyz/', + Origin: 'https://flaxmovies.xyz', + }, + },], + }, + + { + key: 'fsonline', + label: 'FSOnline', + sourceFile: 'fsonline', + proxyParam: 'fo', + timeout: 20000, + retries: 1, + jitter: 0, + multiUrl: true, + skipProxy: true, + }, + + { + key: 'kisskh', + label: 'KissKH', + sourceFile: 'kisskh', + proxyParam: 'kk', + timeout: 30000, + jitter: 500, + retries: 1, + }, + + { + key: 'lmscript', + label: 'LMScript', + sourceFile: 'lmscript', + proxyParam: 'lmsc', + timeout: 20000, + jitter: 500, + retries: 2, + multiUrl: true, + }, + + { + key: 'lookmovie', + label: 'LookMovie', + sourceFile: 'lookmovie', + proxyParam: 'lm', + timeout: 20000, + jitter: 500, + retries: 2, + multiUrl: true, + verifyHeaders: { + 'Accept-Language': 'en-US,en;q=0.9', + }, + }, + + { + key: 'movienight', + label: 'MovieNight', + sourceFile: 'movienight', + proxyParam: 'mn', + timeout: 20000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /movienig\.ht/i, + headers: { + Referer: 'https://movienig.ht/', + Origin: 'https://movienig.ht', + }, + }], + }, + + { + key: 'movy', + label: 'Movy', + sourceFile: 'movy', + proxyParam: 'mv', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /wecollege\.net|movy\.bz/i, + headers: { + Referer: 'https://www.movy.bz/', + Origin: 'https://www.movy.bz', + }, + }], + }, + + { + key: 'pengu', + label: 'Pengu', + sourceFile: 'pengu', + proxyParam: 'pg', + timeout: 25000, + jitter: 500, + retries: 1, + multiUrl: true, + skipProxy: true, + disabled: false, // Enabled, but only returns streams when a manifest is provided + }, + + { + key: 'pstream', + label: 'PStream', + sourceFile: 'pstream', + proxyParam: 'ps', + timeout: 35000, + jitter: 500, + retries: 1, + multiUrl: true, + }, + + { + key: 'purstream', + sourceFile: 'purstream', + label: 'Purstream', + proxyParam: 'ps', + timeout: 20000, + jitter: 500, + retries: 2, + skipProxy: true + }, + + { + key: 'rivestream', + label: 'RiveStream', + sourceFile: 'rivestream', + proxyParam: 'rs', + timeout: 30000, + jitter: 500, + retries: 2, + multiUrl: true, + }, + + { + key: 'streamaggregator', + label: 'StreamAggregator', + sourceFile: 'streamaggregator', + proxyParam: 'sa', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /streamaggregator\.in/i, + headers: { + Referer: 'https://streamaggregator.in/', + Origin: 'https://streamaggregator.in', + }, + }], + }, + { + key: 'vidapi', + label: 'VidAPI', + sourceFile: 'vidapi', + proxyParam: 'va', + timeout: 25000, + jitter: 500, + retries: 1, + multiUrl: true + }, + + { + key: 'vidcore', + label: 'VidCore', + sourceFile: 'vidcore', + proxyParam: 'vc', + timeout: 35000, + jitter: 500, + retries: 1, + multiUrl: true, + }, + { + key: 'vidfast', + label: 'VidFast', + sourceFile: 'vidfast', + proxyParam: 'vf', + timeout: 35000, + jitter: 500, + retries: 1, + multiUrl: true, + }, + + { + key: 'vidgod', + label: 'VidGod', + sourceFile: 'vidgod', + proxyParam: 'vg', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + }, + + { + key: 'vidlink', + label: 'Vidlink', + sourceFile: 'vidlink', + proxyParam: 'vl', + timeout: 20000, + jitter: 500, + retries: 2, + skipProxy: true, + multiUrl: true, + }, + + { + key: 'vidnest', + label: 'VidNest', + sourceFile: 'vidnest', + proxyParam: 'vdn', + timeout: 20000, + retries: 1, + jitter: 0, + multiUrl: true, + }, + + { + key: 'vidnest-sub', + label: 'VidNest (Sub)', + sourceFile: 'vidnest', + proxyParam: 'vdn', + timeout: 20000, + retries: 1, + jitter: 0, + multiUrl: true, + skipProxy: true, + }, + + { + key: 'vidnest-dub', + label: 'VidNest (Dub)', + sourceFile: 'vidnest', + proxyParam: 'vdn', + timeout: 20000, + retries: 1, + jitter: 0, + multiUrl: true, + skipProxy: true, + }, + + { + key: 'vidrock', + label: 'VidRock', + sourceFile: 'vidrock', + proxyParam: 'vr', + timeout: 20000, + jitter: 800, + retries: 3, + multiUrl: true, + cdnHeaders: [{ + pattern: /./, + headers: { + Accept: '/', + 'Accept-Language': 'en-US,en;q=0.9', + Referer: 'https://vidrock.ru/', + Origin: 'https://vidrock.ru', + }, + },], + }, + + { + key: 'vidup', + label: 'VidUp', + sourceFile: 'vidup', + proxyParam: 'vu', + timeout: 35000, + jitter: 500, + retries: 1, + multiUrl: true + }, + + { + key: 'vidvault', + label: 'VidVault', + sourceFile: 'vidvault', + proxyParam: 'vv', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + }, + + { + key: 'vidzee', + label: 'VidZee', + sourceFile: 'vidzee', + proxyParam: 'vz', + timeout: 20000, + sourcesTimeout: 10000, + jitter: 400, + retries: 3, + verifyHeaders: { + Accept: '/', + 'Accept-Language': 'en-US,en;q=0.9', + Referer: 'https://player.vidzee.wtf', + Origin: 'https://player.vidzee.wtf', + }, + }, + + { + key: 'vixsrc', + label: 'VixSrc', + sourceFile: 'vixsrc', + proxyParam: 'vx', + timeout: 35000, + jitter: 0, + retries: 2, + multiUrl: false, + skipProxy: true, + verifyHeaders: { + Accept: 'application/json, text/javascript, /; q=0.01', + 'Accept-Language': 'en-US,en;q=0.9', + Referer: 'https://vixsrc.to/', + Origin: 'https://vixsrc.to', + }, + }, + + { + key: 'xdownloader', + label: 'XDownloader', + sourceFile: 'xdownloader', + proxyParam: 'xd', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /films365\.org/i, + headers: { + Referer: 'https://www.films365.org/', + Origin: 'https://www.films365.org', + }, + }], + }, + +]; + +export const HEALTH_PROBE_ID = '155'; +export const SOURCE_MAP = Object.fromEntries(SOURCES.map(s => [s.key, s])); +export default SOURCES; \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/src/downloads/downloads.js b/node_modules/@vyla-entertainment/sdk/src/downloads/downloads.js new file mode 100644 index 0000000..c764c56 --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/src/downloads/downloads.js @@ -0,0 +1,53 @@ +import SOURCES from "../config.js"; + +export async function fetchDownloads(sdkOrId, idOrS = null, sOrE = null, maybeE = null) { + let sdk = null; + let id = sdkOrId; + let s = idOrS; + let e = sOrE; + + if (sdkOrId && typeof sdkOrId === 'object' && typeof sdkOrId.getStream === 'function') { + sdk = sdkOrId; + id = idOrS; + s = sOrE; + e = maybeE; + } + + if (!sdk || !id) return []; + + const activeSources = SOURCES.filter(cfg => !cfg.disabled && cfg.skipProxy); + const seenUrls = new Set(); + const downloads = []; + + const results = await Promise.allSettled( + activeSources.map(async (cfg) => { + try { + const res = await sdk.getStream(cfg.key, id, s, e); + if (!res) return; + + const items = Array.isArray(res.allUrls) && res.allUrls.length > 0 + ? res.allUrls + : (res.url ? [{ url: res.url, label: cfg.label }] : []); + + for (const item of items) { + if (!item?.url || typeof item.url !== 'string') continue; + const url = item.url.trim(); + + if (!url.startsWith('http') || url.includes('.m3u8')) continue; + if (!/\.mp4(\?|$)/i.test(url) && !url.includes('.mp4')) continue; + if (seenUrls.has(url)) continue; + + seenUrls.add(url); + + downloads.push({ + source: cfg.key, + label: item.label || cfg.label, + url, + }); + } + } catch { } + }) + ); + + return downloads; +} \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/src/health.js b/node_modules/@vyla-entertainment/sdk/src/health.js new file mode 100644 index 0000000..08387be --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/src/health.js @@ -0,0 +1,38 @@ +import { HEALTH_PROBE_ID } from "./config.js"; + +const withTimeout = (promise, ms) => Promise.race([promise, new Promise(r => setTimeout(() => r(null), ms))]); +async function withRetry(fn, attempts = 2, delay = 300) { + for (let i = 0; i < attempts; i++) { + try { + const result = await fn(); + if (result != null) return result; + } catch (err) { + if (i === attempts - 1) throw err; + await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.5)); + } + } + return null; +} + +export async function probeSource(sdk, cfg, mod) { + const audio = /dub$/.test(cfg.key) ? 'dub' : 'sub'; + const streamArgs = { + id: HEALTH_PROBE_ID, + s: null, + e: null, + clientIP: null, + absoluteBase: null, + audio, + sdk, + config: cfg, + }; + + const t = Date.now(); + const result = await withTimeout( + withRetry(() => mod.getStream(streamArgs), cfg.retries, 300), + cfg.timeout + ); + + const firstUrl = result?.url ?? result?.allUrls?.[0]?.url; + return { ok: !!(typeof firstUrl === 'string' && firstUrl.startsWith('http')), ms: Date.now() - t }; +} \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/src/sdk.d.ts b/node_modules/@vyla-entertainment/sdk/src/sdk.d.ts new file mode 100644 index 0000000..0f27998 --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/src/sdk.d.ts @@ -0,0 +1,122 @@ +export interface SourceConfig { + key: string; + label: string; + sourceFile: string; + proxyParam: string; + timeout: number; + jitter: number; + retries: number; + multiUrl: boolean; + skipProxy?: boolean; + cdnHeaders?: { + pattern: RegExp; + headers: { + Referer: string; + Origin: string; + }; + }[]; + skipVerify?: boolean; + verifyHeaders?: boolean; + disabled?: boolean; + sourcesTimeout?: number; +} +export interface ProbeResult { + ok: boolean; + ms: number | null; +} +export interface StreamResult { + url: string; + headers: { + "User-Agent": string; + Referer: string; + Origin: string; + [key: string]: string; + }; + server: string; + quality: string; + type: "mp4" | "hls"; + skipVerify?: boolean; + skipProxy?: boolean; +} + +export interface DownloadResult { + url: string; + quality: string; + size: number | null; + type: "mkv"; + active: boolean; +} +export interface SubtitleResult { + label: string; + file: string; + type: "vtt" | "srt"; + source: string; +} + +export type ProbeAllResult = Record; + +export interface VylaSDKOptions { + tmdbApiKey?: string; +} + +export default class VylaSDK { + tmdbApiKey?: string; + + constructor(options?: VylaSDKOptions); + + /** + * Get the list of configured sources. + * @param excludeDisabled If true, filters out disabled sources. + */ + getSources(excludeDisabled?: boolean): SourceConfig[]; + + /** + * Probe all active (non-disabled) sources for availability. + */ + probeAllSources(): Promise; + + /** + * Probe a single source by key. + */ + probeSource(key: string): Promise; + + /** + * Get a playable stream for a given source/media id. + * @param key Source key + * @param id Media id (e.g. TMDB id) + * @param s Season number (for TV), or null for movies + * @param e Episode number (for TV), or null for movies + * @param clientIP Optional client IP for source-specific logic + */ + getStream( + key: string, + id: string | number, + s?: number | null, + e?: number | null, + clientIP?: string | null + ): Promise; + + /** + * Get subtitles for a movie or TV episode. + * @param id Media id + * @param s Season number (for TV), or null for movies + * @param e Episode number (for TV), or null for movies + */ + getSubtitles( + id: string | number, + s?: number | null, + e?: number | null + ): Promise; + + /** + * Get download links for a movie or TV episode. + * @param id Media id + * @param s Season number (for TV), or null for movies + * @param e Episode number (for TV), or null for movies + */ + getDownloads( + id: string | number, + s?: number | null, + e?: number | null + ): Promise; +} \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/src/sdk.js b/node_modules/@vyla-entertainment/sdk/src/sdk.js new file mode 100644 index 0000000..1212e4e --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/src/sdk.js @@ -0,0 +1,68 @@ +import SOURCES, { SOURCE_MAP } from "./config.js"; +import { fetchDownloads } from "./downloads/downloads.js"; +import { probeSource } from "./health.js"; +import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js"; +import { createStreamArgs } from "./utils/helpers.js"; + +const ALL_SOURCE_MODULES = Object.fromEntries( + await Promise.all(SOURCES.map(async cfg => [cfg.key, await import(`./sources/${cfg.sourceFile}.js`)])) +); + +const SOURCE_MODULES = Object.fromEntries( + Object.entries(ALL_SOURCE_MODULES).filter(([key]) => !SOURCE_MAP[key]?.disabled) +); + +export default class VylaSDK { + tmdbApiKey = null; + penguManifest = null; + constructor({ tmdbApiKey, penguManifest } = {}) { + this.tmdbApiKey = tmdbApiKey || null; + this.penguManifest = penguManifest || null; + } + getSources(excludeDisabled = false) { + if (excludeDisabled) { + return SOURCES.filter(cfg => !cfg.disabled); + } + return SOURCES; + } + async probeAllSources() { + const active = SOURCES.filter(cfg => !cfg.disabled); + + const results = await Promise.allSettled( + active.map(async cfg => await probeSource(cfg, SOURCE_MODULES[cfg.key])) + ); + + const sources = Object.fromEntries( + active.map((cfg, i) => [ + cfg.key, + results[i].status === 'fulfilled' ? results[i].value : { ok: false, ms: null }, + ]) + ); + + return sources; + } + async probeSource(key) { + const cfg = SOURCES.find(cfg => cfg.key === key); + if (!cfg) throw new Error(`Source with key "${key}" not found`); + const mod = SOURCE_MODULES[key]; + if (!mod) throw new Error(`Source module for key "${key}" not found`); + return await probeSource(this, cfg, mod); + } + async getStream(key, id, s = null, e = null, clientIP = null) { + const cfg = SOURCES.find(cfg => cfg.key === key); + if (!cfg) throw new Error(`Source with key "${key}" not found`); + const mod = SOURCE_MODULES[key]; + if (!mod) throw new Error(`Source module for key "${key}" not found`); + const streamArgs = await createStreamArgs(cfg, this, id, s, e, clientIP); + return await mod.getStream(streamArgs); + } + async getSubtitles(id, s = null, e = null) { + const paths = s != null && e != null + ? await getSubPathsTv(id, s, e) + : await getSubPathsMovie(id); + return await fetchSubtitles(paths); + } + async getDownloads(id, s = null, e = null) { + return await fetchDownloads(this, id, s, e); + } +} \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/src/sources/111477.js b/node_modules/@vyla-entertainment/sdk/src/sources/111477.js new file mode 100644 index 0000000..ca88abd --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/src/sources/111477.js @@ -0,0 +1,101 @@ +import { getUA } from '../utils/helpers.js'; + +const SERVICE_ORIGIN = 'https://st.111477.xyz'; + +function generateManifestBaseUrl(tmdbKey) { + let config = 'https://a.111477.xyz/::sort=file-desc::limit=3'; + if (tmdbKey) { + config += `::tmdb=${tmdbKey}`; + } + const b64 = Buffer.from(config, 'utf8') + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + return `${SERVICE_ORIGIN}/config/${b64}`; +} + +async function resolveImdbId(sdk, id, isTv) { + const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY; + if (!apiKey) return null; + const type = isTv ? 'tv' : 'movie'; + try { + const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { + signal: AbortSignal.timeout(6000), + }); + if (!res.ok) return null; + const data = await res.json(); + return data.imdb_id || data.external_ids?.imdb_id || null; + } catch { + return null; + } +} + +export async function getStream({ id, s, e, sdk }) { + const isTv = Boolean(s && e); + const ua = getUA(); + const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY; + + const imdbId = await resolveImdbId(sdk, id, isTv); + const targetIds = []; + + if (imdbId && imdbId.startsWith('tt')) { + targetIds.push(imdbId); + } + targetIds.push(`tmdb:${id}`); + + const addonBase = generateManifestBaseUrl(apiKey); + const seenUrls = new Set(); + const allUrls = []; + + const defaultHeaders = { + 'User-Agent': ua, + Accept: 'application/json, text/plain, */*', + }; + + for (const targetId of targetIds) { + const endpoint = isTv + ? `${addonBase}/stream/series/${targetId}:${s || 1}:${e || 1}.json` + : `${addonBase}/stream/movie/${targetId}.json`; + + try { + const res = await fetch(endpoint, { + headers: defaultHeaders, + signal: AbortSignal.timeout(12000), + }); + + if (!res.ok) continue; + const data = await res.json(); + + if (Array.isArray(data?.streams) && data.streams.length > 0) { + for (const item of data.streams) { + if (!item || typeof item !== 'object') continue; + const url = String(item.url || '').trim(); + if (!url || !url.startsWith('http') || seenUrls.has(url)) continue; + + seenUrls.add(url); + + const rawTitle = item.title ? String(item.title).trim() : ''; + const rawName = item.name ? String(item.name).trim() : '111477'; + const label = rawTitle || rawName; + + allUrls.push({ + url, + label, + headers: defaultHeaders, + }); + } + + if (allUrls.length > 0) break; + } + } catch { } + } + + if (allUrls.length === 0) return null; + + return { + url: allUrls[0].url, + allUrls, + headers: allUrls[0].headers, + }; +} \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/src/sources/123anime.js b/node_modules/@vyla-entertainment/sdk/src/sources/123anime.js new file mode 100644 index 0000000..a0fa6dd --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/src/sources/123anime.js @@ -0,0 +1,57 @@ +import { getTmdbInfo, fetchJson, USER_AGENT } from '../utils/helpers.js'; + +async function resolveSlug(id, s, e, info) { + const titles = info?.titles || []; + const keywords = []; + for (const title of titles) { + const t = title.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim(); + if (!keywords.includes(t)) keywords.push(t); + const words = title.split(/[\s:]+/).filter(w => w.length > 2); + if (words.length >= 2) { + const short = words.slice(0, 3).join(' ').toLowerCase(); + if (!keywords.includes(short)) keywords.push(short); + } + } + const slugCandidates = []; + for (const keyword of keywords) { + try { + const data = await fetchJson(`https://123animehub.cc/ajax/film/search?keyword=${encodeURIComponent(keyword)}&_=${Date.now()}`, { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Referer': 'https://123animehub.cc', 'Accept': 'application/json', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(8000) }); + let html = data.html || '', idx = 0; + while ((idx = html.indexOf('href="/anime/', idx)) !== -1) { + const start = idx + 13; + const end = html.indexOf('"', start); + const slug = html.slice(start, end); + if (!slugCandidates.includes(slug)) slugCandidates.push(slug); + idx = end; + } + } catch { } + if (slugCandidates.length) break; + } + for (const slug of slugCandidates) { + try { + const data = await fetchJson(`https://123animehub.cc/ajax/episode/info?epr=${encodeURIComponent(s ? `${slug}/${s}/${e}` : `${slug}/1`)}&ts=1&_=${Date.now()}`, { headers: { 'Referer': `https://123animehub.cc/anime/${slug}`, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) }); + if (data?.target) return data.target; + } catch { } + } + return null; +} + +export async function getStream({ id, s, e }) { + if (!s) return null; + const info = await getTmdbInfo(id, 'tv', s); + if (!info.isAnime) return null; + const target = await resolveSlug(id, s, e, info); + if (!target) return null; + const embedMatch = target.match(/\/embed-[^/]+\/([A-Za-z0-9+/=]+)$/); + if (!embedMatch) return null; + const origin = new URL(target).origin; + try { + const embedRes = await fetch(target, { headers: { 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) }); + if (!embedRes.ok) return null; + const setCookie = embedRes.headers.get('set-cookie'); + const data = await fetchJson(`${origin}/hs/getSources?id=${embedMatch[1]}`, { headers: { 'Referer': target, 'Accept': '*/*', 'User-Agent': USER_AGENT, ...(setCookie ? { 'Cookie': setCookie.split(';')[0].trim() } : {}) }, signal: AbortSignal.timeout(10000) }); + const src = typeof data.sources === 'string' && data.sources.length ? data.sources : (Array.isArray(data.sources) && data.sources.length ? (data.sources[0].file || data.sources[0].src || data.sources[0].url) : null); + if (src) return { url: src, headers: { 'Referer': 'https://play2.echovideo.ru/', 'Origin': 'https://play2.echovideo.ru' } }; + } catch { } + return null; +} \ No newline at end of file diff --git a/node_modules/@vyla-entertainment/sdk/src/sources/anihq.js b/node_modules/@vyla-entertainment/sdk/src/sources/anihq.js new file mode 100644 index 0000000..bfdd461 --- /dev/null +++ b/node_modules/@vyla-entertainment/sdk/src/sources/anihq.js @@ -0,0 +1,124 @@ +import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js'; + +async function extractVoe(voeUrl) { + try { + let res = await fetch(voeUrl, { + headers: { + 'User-Agent': USER_AGENT, + 'Referer': 'https://anihq.cc/' + } + }); + + let html = await res.text(); + + const redirectMatch = html.match(/window\.location\.href\s*=\s*['"](https?:\/\/[^'"]+)['"]/); + if (redirectMatch) { + res = await fetch(redirectMatch[1], { + headers: { + 'User-Agent': USER_AGENT, + 'Referer': 'https://anihq.cc/' + } + }); + html = await res.text(); + } + + const mirrorUrl = res.url; + + const scriptMatch = html.match(/