From afc2246e787af303c93b9447a4c20f15b8bfac6f Mon Sep 17 00:00:00 2001 From: alwaysmesmerizingyou Date: Sun, 4 Oct 2026 20:01:41 -0700 Subject: [PATCH] Initial commit --- android/app/src/main/AndroidManifest.xml | 31 + assets/favicon.ico | Bin 0 -> 27557 bytes capacitor.config.json | 9 + css/base.css | 315 +++++++ css/detail.css | 432 ++++++++++ css/library.css | 65 ++ css/player.css | 715 ++++++++++++++++ css/search.css | 268 ++++++ css/sidebar.css | 114 +++ css/skeleton.css | 213 +++++ index.html | 90 ++ js/browse.js | 57 ++ js/core.js | 37 + js/detail.js | 66 ++ js/focus.js | 60 ++ js/input.js | 99 +++ js/library.js | 27 + js/main.js | 8 + js/player.js | 805 ++++++++++++++++++ js/search.js | 70 ++ js/sidebar.js | 16 + js/skeleton.js | 13 + node_modules/.package-lock.json | 14 + .../@vyla-entertainment/sdk/.env.example | 1 + .../sdk/.github/workflows/publish.yml | 38 + .../@vyla-entertainment/sdk/.gitlab-ci.yml | 16 + node_modules/@vyla-entertainment/sdk/LICENSE | 407 +++++++++ .../@vyla-entertainment/sdk/README.md | 31 + .../@vyla-entertainment/sdk/package.json | 33 + .../@vyla-entertainment/sdk/src/config.js | 487 +++++++++++ .../sdk/src/downloads/downloads.js | 53 ++ .../@vyla-entertainment/sdk/src/health.js | 38 + .../@vyla-entertainment/sdk/src/sdk.d.ts | 122 +++ .../@vyla-entertainment/sdk/src/sdk.js | 68 ++ .../sdk/src/sources/111477.js | 101 +++ .../sdk/src/sources/123anime.js | 57 ++ .../sdk/src/sources/anihq.js | 124 +++ .../sdk/src/sources/anineko.js | 190 +++++ .../sdk/src/sources/bcine.js | 202 +++++ .../sdk/src/sources/dulo.js | 150 ++++ .../sdk/src/sources/flaxmovies.js | 118 +++ .../sdk/src/sources/fsonline.js | 48 ++ .../sdk/src/sources/kisskh.js | 60 ++ .../sdk/src/sources/lmscript.js | 43 + .../sdk/src/sources/lookmovie.js | 80 ++ .../sdk/src/sources/movienight.js | 117 +++ .../sdk/src/sources/movy.js | 282 ++++++ .../sdk/src/sources/pengu.js | 50 ++ .../sdk/src/sources/pstream.js | 68 ++ .../sdk/src/sources/purstream.js | 104 +++ .../sdk/src/sources/rivestream.js | 179 ++++ .../sdk/src/sources/streamaggregator.js | 129 +++ .../sdk/src/sources/vidapi.js | 115 +++ .../sdk/src/sources/vidcore.js | 123 +++ .../sdk/src/sources/vidfast.js | 123 +++ .../sdk/src/sources/vidgod.js | 147 ++++ .../sdk/src/sources/vidlink.js | 28 + .../sdk/src/sources/vidnest.js | 79 ++ .../sdk/src/sources/vidrock.js | 145 ++++ .../sdk/src/sources/vidup.js | 123 +++ .../sdk/src/sources/vidvault.js | 188 ++++ .../sdk/src/sources/vidzee.js | 184 ++++ .../sdk/src/sources/vixsrc.js | 112 +++ .../sdk/src/sources/xdownloader.js | 129 +++ .../@vyla-entertainment/sdk/src/subtitles.js | 122 +++ .../sdk/src/utils/helpers.js | 230 +++++ package-lock.json | 22 + package.json | 25 + server.js | 348 ++++++++ 69 files changed, 9163 insertions(+) create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 assets/favicon.ico create mode 100644 capacitor.config.json create mode 100644 css/base.css create mode 100644 css/detail.css create mode 100644 css/library.css create mode 100644 css/player.css create mode 100644 css/search.css create mode 100644 css/sidebar.css create mode 100644 css/skeleton.css create mode 100644 index.html create mode 100644 js/browse.js create mode 100644 js/core.js create mode 100644 js/detail.js create mode 100644 js/focus.js create mode 100644 js/input.js create mode 100644 js/library.js create mode 100644 js/main.js create mode 100644 js/player.js create mode 100644 js/search.js create mode 100644 js/sidebar.js create mode 100644 js/skeleton.js create mode 100644 node_modules/.package-lock.json create mode 100644 node_modules/@vyla-entertainment/sdk/.env.example create mode 100644 node_modules/@vyla-entertainment/sdk/.github/workflows/publish.yml create mode 100644 node_modules/@vyla-entertainment/sdk/.gitlab-ci.yml create mode 100644 node_modules/@vyla-entertainment/sdk/LICENSE create mode 100644 node_modules/@vyla-entertainment/sdk/README.md create mode 100644 node_modules/@vyla-entertainment/sdk/package.json create mode 100644 node_modules/@vyla-entertainment/sdk/src/config.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/downloads/downloads.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/health.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sdk.d.ts create mode 100644 node_modules/@vyla-entertainment/sdk/src/sdk.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/111477.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/123anime.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/anihq.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/anineko.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/bcine.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/dulo.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/flaxmovies.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/fsonline.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/kisskh.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/lmscript.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/lookmovie.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/movienight.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/movy.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/pengu.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/pstream.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/purstream.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/rivestream.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/streamaggregator.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vidapi.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vidcore.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vidfast.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vidgod.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vidlink.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vidnest.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vidrock.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vidup.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vidvault.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vidzee.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/vixsrc.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/sources/xdownloader.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/subtitles.js create mode 100644 node_modules/@vyla-entertainment/sdk/src/utils/helpers.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server.js 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 0000000000000000000000000000000000000000..9fdbb8205965d3c4c4dd0726794419698d82462b GIT binary patch literal 27557 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ΝY@TCG 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 F{y*cuqg?<1 literal 0 HcmV?d00001 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(/