Initial commit

This commit is contained in:
2026-10-04 20:01:41 -07:00
commit afc2246e78
69 changed files with 9163 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:banner="@mipmap/ic_launcher">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

BIN
assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

9
capacitor.config.json Normal file
View File

@@ -0,0 +1,9 @@
{
"appId": "tv.vyla.app",
"appName": "Vyla",
"webDir": ".",
"server": {
"androidScheme": "https",
"cleartext": true
}
}

315
css/base.css Normal file
View File

@@ -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
}
}

432
css/detail.css Normal file
View File

@@ -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)
}

65
css/library.css Normal file
View File

@@ -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
}

715
css/player.css Normal file
View File

@@ -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;
}

268
css/search.css Normal file
View File

@@ -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
}

114
css/sidebar.css Normal file
View File

@@ -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
}

213
css/skeleton.css vendored Normal file
View File

@@ -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
}

90
index.html Normal file
View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vyla</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/sidebar.css">
<link rel="stylesheet" href="css/detail.css">
<link rel="stylesheet" href="css/search.css">
<link rel="stylesheet" href="css/library.css">
<link rel="stylesheet" href="css/player.css">
<link rel="stylesheet" href="css/skeleton.css">
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head>
<body>
<div id="bg"></div>
<div id="dim"></div>
<div id="dim2"></div>
<div id="main">
<div class="hero">
<div id="hb"></div>
<div class="hc sl">
<div class="hskel"><i class="sb t"></i><i class="sb m"></i><i class="sb l"></i><i class="sb l"></i><i
class="sb l x"></i>
<div class="hb"><i class="sb b"></i><i class="sb b c"></i></div>
</div>
<div id="hlogo"></div>
<div class="meta" id="hmeta"></div>
<p class="ov1" id="hov"></p>
<div class="hb"><button class="pb f" id="hplay">&#9654; Play</button><button class="ib f"
id="hinfo">&#9432; More Info</button></div>
</div>
</div>
<div id="rows"></div>
<div id="sentinel"></div>
</div>
<div id="sr">
<div class="stop"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.5-3.5" />
</svg><span id="stext">Search</span></div>
<div id="kb"></div>
<div class="row ch" id="chips"></div>
<div class="tabs" id="tabs"></div>
<div id="sres"></div>
</div>
<div id="lib">
<h1 id="lt"></h1>
<div class="grid" id="lgrid"></div>
</div>
<aside id="side">
<div class="brand"><img id="mark" alt="Vyla"><span>Vyla</span></div>
<nav id="nav"></nav>
</aside>
<div id="ov"></div>
<div id="pl">
<video id="pbg" playsinline preload="auto" muted></video>
<video id="pv" playsinline preload="auto"></video>
<div class="top" id="ptop"></div>
<div class="f" id="pmid"></div>
<div id="pact">
<div class="skip f" id="pskip">Skip Intro</div>
<div class="skip f" id="pnext"></div>
</div>
<div class="bot" id="pbot"></div>
<div id="pep"></div>
<div id="pspin"></div>
<div id="pm"></div>
</div>
<script src="js/core.js"></script>
<script src="js/skeleton.js"></script>
<script src="js/focus.js"></script>
<script src="js/library.js"></script>
<script src="js/sidebar.js"></script>
<script src="js/browse.js"></script>
<script src="js/search.js"></script>
<script src="js/detail.js"></script>
<script src="js/player.js"></script>
<script src="js/input.js"></script>
<script src="js/main.js"></script>
</body>
</html>

57
js/browse.js Normal file
View File

@@ -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 = `<h1>${t}</h1>`;
$('#hmeta').innerHTML = `<span>&#9733; ${(it.vote_average || 0).toFixed(1)}</span><span>${yr(it)}</span><span class="b">${it._t === 'movie' ? 'Movie' : 'Series'}</span><span>${gn(it)}</span>`;
$('#hov').textContent = it.overview || '';
const l = await logo(it);
if (heroIt === it && l) $('#hlogo').innerHTML = `<img src="${l}" alt="${t.replace(/"/g, '')}">`
}
function card(it, h = true) {
const c = document.createElement('div'); c.className = 'card f'; c.tabIndex = -1;
c.innerHTML = `<img loading="lazy" src="${I}w342${it.poster_path}" alt="">`;
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 = `<h2>${g.name} ${mv ? 'Movies' : 'TV Shows'}</h2><div class="row">${SK.cards(14)}</div>`;
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);

37
js/core.js Normal file
View File

@@ -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 => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${p}</svg>`;
const P_WL = '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>', P_FV = '<path d="M20.8 5.6a5.5 5.5 0 0 0-7.8 0L12 6.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1.1L12 22l7.8-7.5 1-1.1a5.5 5.5 0 0 0 0-7.8z"/>';
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(/</g, '&lt;');
const mkf = (cls, html, fn) => { 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('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><rect width="40" height="40" rx="11" fill="#2a2a34"/><circle cx="14" cy="14" r="3" fill="#5b5b6b"/><path d="M7 31l9-10 6 6 4-4 7 8z" fill="#5b5b6b"/></svg>');
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);

66
js/detail.js Normal file
View File

@@ -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 = `<div class="dbl" style="background-image:url(${I}w780${bp})"></div><div class="dw"><div class="dimg" style="background-image:url(${I}w1280${bp})"></div><div class="dc"><div class="dh"><div id="dlogo">${lg ? `<img src="${I}w500${lg.file_path}" alt="">` : `<h1>${title}</h1>`}</div><div class="meta"><span>${(d.vote_average || 0).toFixed(1)}</span><span>${year}</span>${cert ? `<span class="b">${cert}</span>` : ''}<span>${rt}</span><span>${(d.genres || []).slice(0, 3).map(g => g.name).join(', ')}</span></div><div class="hb"><button class="pb f" id="dplay">Play</button><div class="circ f" id="dmenu">&#8942;</div><div class="mx" id="dmx"><div class="circ f" id="dwl"></div><div class="circ f" id="dfv"></div></div></div><p class="otx" id="otx">${d.overview || ''}</p><span class="more f" id="dmore">MORE</span></div><div id="dsecs"></div></div></div>`;
const secs = $('#dsecs');
const rowOf = (name, kids) => { if (!kids.length) return; const s = document.createElement('section'); s.className = 'ds'; s.innerHTML = `<h3>${name}</h3><div class="row"></div>`; 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 = '<div class="eh"><h3>Episodes</h3><div class="dd"><div class="ddb f" id="ddb"></div><div class="ddl" id="ddl"></div></div></div><div class="el" id="el"></div>';
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 = `<span>${esc(nm(x))}</span>` + ic('<path d="m6 9 6 6 6-6"/>');
[...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', `<div class="et"><img loading="lazy" src="${img}" alt=""></div><b>${esc(p.name || 'Episode ' + n)}</b><small>${['Episode ' + n, p.runtime ? p.runtime + 'm' : ''].filter(Boolean).join(' \u2022 ')}</small>`);
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 = '<p class="none">No episodes listed.</p>'
};
ss.forEach(x => ddl.appendChild(mkf('ddi', `<span>${esc(nm(x))}</span><small>${x.episode_count} eps</small>`, () => { 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', `<div class="tb" style="background-image:url(https://img.youtube.com/vi/${v.key}/mqdefault.jpg)"></div><div class="th" style="background-image:url(https://img.youtube.com/vi/${v.key}/mqdefault.jpg)"></div><b>${v.name}</b><small>&#9654; ${v.type}</small>`)));
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', `<div class="pf" style="${p.profile_path ? `background-image:url(${I}w185${p.profile_path})` : ''}">${p.profile_path ? '' : p.name[0]}</div><b>${p.name}</b><small>${p.character || ''}</small>`)));
$('#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 }

60
js/focus.js Normal file
View File

@@ -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() }

99
js/input.js Normal file
View File

@@ -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 });

27
js/library.js Normal file
View File

@@ -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]}<small>${a.length} title${a.length === 1 ? '' : 's'}</small>`;
g.innerHTML = '';
if (!a.length) { g.innerHTML = '<p class="none">Nothing here yet. Open a title, press the \u22EE button and add it.</p>'; 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')) }
}

8
js/main.js Normal file
View File

@@ -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 = '<p style="padding:40vh var(--pad)">Could not reach TMDB.</p>'; return }
await go('discover');
})();

805
js/player.js Normal file
View File

@@ -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) => `<svg viewBox="0 0 24 24" fill="${f ? 'currentColor' : 'none'}" stroke="${f ? 'none' : 'currentColor'}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">${p}</svg>`;
const SPK = '<path d="M4 9v6h4l5 4V5L8 9z"/>';
const PI = {
x: pi('<path d="M5 5l14 14M19 5 5 19"/>'),
play: pi('<path d="M7 4.5v15L19.5 12z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>', 1),
pause: pi('<rect x="6" y="4.5" width="4" height="15" rx="1.2"/><rect x="14" y="4.5" width="4" height="15" rx="1.2"/>', 1),
cc: pi('<rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10a2 2 0 1 0 0 4M17 10a2 2 0 1 0 0 4"/>'),
vol: pi(SPK + '<path d="M16.5 8.5a5 5 0 0 1 0 7M19 6a9 9 0 0 1 0 12"/>'),
mute: pi(SPK + '<path d="m17 9.5 4 5m0-5-4 5"/>'),
qlt: pi('<path d="M4 6h16M4 12h16M4 18h7"/>'),
set: pi('<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>'),
more: pi('<path d="M5 12h.01M12 12h.01M19 12h.01" stroke-width="3.4"/>'),
fs: pi('<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>'),
fx: pi('<path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/>'),
eps: pi('<rect x="3" y="4" width="7" height="6" rx="1.5"/><rect x="14" y="4" width="7" height="6" rx="1.5"/><rect x="3" y="14" width="7" height="6" rx="1.5"/><rect x="14" y="14" width="7" height="6" rx="1.5"/>'),
chk: pi('<path d="m5 12.5 4.5 4.5L19 7"/>')
};
$('#ptop').innerHTML = `<div class="pic f" id="pback">${PI.x}</div><img class="av" id="pav" alt=""><div class="pt"><b id="pt"></b><small id="ps"></small></div><div class="pr"><div class="pic f" id="pqlt" title="Quality">${PI.qlt}</div><div class="pic f" id="pcc">${PI.cc}</div><div class="pic f" id="pvol">${PI.vol}</div><div class="pic f" id="pset">${PI.set}</div><div class="pic f" id="pmore">${PI.more}</div></div>`;
$('#pbot').innerHTML = `<span class="bt" id="pcur">0:00</span><div class="sk f" id="pseek"><u id="pbuf"></u><i id="pfill"></i><b id="phd"></b></div><span class="bt" id="pdur">0:00</span><div class="pic f" id="peps">${PI.eps}</div><div class="pic f" id="pfs">${PI.fs}</div>`;
$('#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 = '<div style="padding:10px 14px;color:rgba(255,255,255,0.4);font-size:13px;">Probing providers...</div>';
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 ? `<span class="src-expand-btn">${expandedSources.has(s.provider) ? '▲' : '▼'}</span>` : '';
hdr.innerHTML = `<span>${s.label}${countLabel}</span><div>${expandIcon}${s.provider === selectedSourceKey ? PI.chk : ''}</div>`;
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 = `<span>${u.quality || 'Stream ' + (i + 1)}</span>${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', `<span>${l}</span>${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 = `<span>Timing Offset</span><div class="sub-btn-group"><button id="sub-min">-0.5s</button><button id="sub-plus">+0.5s</button></div>`;
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 = '<div class="pic f" id="vm"></div><div class="vs f" id="vsl"><i></i></div><span id="vpc"></span>';
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<small>${esc(c.it.title || c.it.name || '')}</small>`;
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' : ''), `<div class="et"><img loading="lazy" src="${img}" alt=""><i>${n}</i>${cur ? '<span class="eq"><u></u><u></u><u></u></span><span class="pg"><u></u></span>' : ''}</div><span class="pn">${esc(p.name || 'Episode ' + n)}</span><small>${p.runtime ? p.runtime + 'm' : 'Episode ' + n}</small>`, () => playEp(sn, i)));
});
const t = car.querySelector('.cur') || car.firstChild;
if (focus && t) setFocus(t);
}
$('#pep').innerHTML = `<div class="peh"><h3 id="peT"></h3><div class="row" id="pseas"></div><div class="pic f" id="pepx">${PI.x}</div></div><div class="row pcar" id="pcar"></div>`;
$('#pnext').innerHTML = 'Next Episode' + pi('<path d="M6 5.5v13l9-6.5z"/><path d="M18 5.5v13"/>');
$('#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();
});

70
js/search.js Normal file
View File

@@ -0,0 +1,70 @@
let sqv = '', bgT, stab = 0, SD = { q: '', all: [], mv: [], tv: [] };
const SVS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>';
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', `<div class="po"><img src="${I}w342${x.poster_path}" alt=""></div><b>${esc(x.title || x.name || '')}</b><small><span>&#9733; ${(x.vote_average || 0).toFixed(1)}</span><span>${yr(x)}</span><span>${x.media_type === 'tv' ? 'Show' : 'Movie'}</span></small>`, () => 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 = `<h3>${h}</h3><div class="row ch"></div>`; sec.lastChild.append(...a.map(tile)); sres.appendChild(sec) });
if (!n) sres.innerHTML = `<p class="none">No results${q ? ` for \u201c${esc(q)}\u201d` : ''}</p>`;
[...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('<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5h11a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9l-6-7z"/><path d="m12 9 5 5m0-5-5 5"/></svg>', '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(() => `<section class="ss"><i class="sb h3s"></i><div class="row ch">${SK.tiles(9)}</div></section>`).join('')
}

16
js/sidebar.js Normal file
View File

@@ -0,0 +1,16 @@
const NAV = [
['search', 'Search', ic('<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>')],
['discover', 'Discovery', ic('<circle cx="12" cy="12" r="9"/><path d="m15.5 8.5-2 5-5 2 2-5z"/>')],
['movie', 'Movies', ic('<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 3v18M17 3v18M3 8h4M3 12h4M3 16h4M17 8h4M17 12h4M17 16h4"/>')],
['tv', 'TV Shows', ic('<rect x="3" y="7" width="18" height="13" rx="2"/><path d="m8 3 4 4 4-4"/>')],
['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 + `<span>${n}</span>`; 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' }) }

13
js/skeleton.js Normal file
View File

@@ -0,0 +1,13 @@
const SK = {
cards: n => '<div class="card sl"></div>'.repeat(n),
tiles: n => '<div class="sc sl"><div class="po"></div><i class="sb"></i><i class="sb s"></i></div>'.repeat(n),
chips: n => Array.from({ length: n }, (_, i) => `<div class="chip sl" style="width:${90 + (i * 37) % 80}px"></div>`).join(''),
pep: n => '<div class="pe sl"><div class="et"></div><i class="sb" style="width:70%;height:14px;margin-top:11px"></i><i class="sb" style="width:35%;height:11px;margin-top:6px"></i></div>'.repeat(n),
eps: n => '<div class="ep sl"><div class="et"></div><i class="sb" style="width:70%;height:14px;margin-top:12px"></i><i class="sb" style="width:40%;height:11px;margin-top:6px"></i></div>'.repeat(n),
rows: n => Array.from({ length: n }, () => '<section class="sec"><i class="sb h2"></i><div class="row">' + SK.cards(14) + '</div></section>').join(''),
detail: it => {
const bp = it.backdrop_path || it.poster_path || '';
return `<div class="dbl" style="background-image:url(${I}w780${bp})"></div><div class="dw"><div class="dimg" style="background-image:url(${I}w1280${bp})"></div><div class="dc"><div class="dh dsk"><i class="sb t"></i><i class="sb m"></i><div class="hb"><i class="sb b"></i><i class="sb c"></i></div><i class="sb l"></i><i class="sb l"></i><i class="sb l x"></i></div>` +
`<div class="ds"><i class="sb h3"></i><div class="row">${'<div class="trk"></div>'.repeat(4)}</div></div><div class="ds"><i class="sb h3"></i><div class="row">${SK.cards(9)}</div></div></div></div>`
}
};

14
node_modules/.package-lock.json generated vendored Normal file
View File

@@ -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"
}
}
}

1
node_modules/@vyla-entertainment/sdk/.env.example generated vendored Normal file
View File

@@ -0,0 +1 @@
TMDB_API_KEY=your_tmdb_api_key_here

View File

@@ -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 }}

16
node_modules/@vyla-entertainment/sdk/.gitlab-ci.yml generated vendored Normal file
View File

@@ -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

407
node_modules/@vyla-entertainment/sdk/LICENSE generated vendored Normal file
View File

@@ -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.

31
node_modules/@vyla-entertainment/sdk/README.md generated vendored Normal file
View File

@@ -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

33
node_modules/@vyla-entertainment/sdk/package.json generated vendored Normal file
View File

@@ -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"
}
}

487
node_modules/@vyla-entertainment/sdk/src/config.js generated vendored Normal file
View File

@@ -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;

View File

@@ -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;
}

38
node_modules/@vyla-entertainment/sdk/src/health.js generated vendored Normal file
View File

@@ -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 };
}

122
node_modules/@vyla-entertainment/sdk/src/sdk.d.ts generated vendored Normal file
View File

@@ -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<string, ProbeResult>;
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<ProbeAllResult>;
/**
* Probe a single source by key.
*/
probeSource(key: string): Promise<ProbeResult>;
/**
* 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<StreamResult | { allUrls: StreamResult[] }>;
/**
* 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<SubtitleResult[]>;
/**
* 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<DownloadResult[]>;
}

68
node_modules/@vyla-entertainment/sdk/src/sdk.js generated vendored Normal file
View File

@@ -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);
}
}

View File

@@ -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,
};
}

View File

@@ -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;
}

View File

@@ -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(/<script type="application\/json">\["(.*?)"\]<\/script>/);
if (!scriptMatch) return null;
let str = scriptMatch[1];
str = str.replace(/[a-zA-Z]/g, (c) => {
return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
});
const junk = ['@$', '^^', '~@', '%?', '*~', '!!', '#&'];
junk.forEach(j => { str = str.split(j).join(''); });
str = Buffer.from(str, 'base64').toString('utf-8');
let shifted = '';
for (let i = 0; i < str.length; i++) {
shifted += String.fromCharCode(str.charCodeAt(i) - 3);
}
let reversed = shifted.split('').reverse().join('');
let finalJsonStr = Buffer.from(reversed, 'base64').toString('utf-8');
const finalData = JSON.parse(finalJsonStr);
if (finalData.file || finalData.source) {
return {
url: finalData.file || finalData.source,
mirror: mirrorUrl
};
}
} catch (e) {
return null;
}
return null;
}
function cleanSlug(title) {
return title
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '-')
.replace(/^-+|-+$/g, '');
}
export async function getStream({ id, s, e, audio }) {
if (!s) return null;
const info = await getTmdbInfo(id, 'tv', s);
if (!info.isAnime) return null;
const seriesTitle = info.titles.find(t =>
t.toLowerCase() !== 'east blue' &&
t.length > 3
) || info.titles[0];
const cleanTitle = cleanSlug(seriesTitle);
const typeSuffix = audio === 'dub' ? 'english-dubbed' : 'english-subbed';
const url = `https://anihq.cc/watch/${cleanTitle}-episode-${e}-${typeSuffix}/`;
try {
let pageRes = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
let html = await pageRes.text();
if (!pageRes.ok) {
const searchUrl = `https://anihq.cc/search?keyword=${encodeURIComponent(seriesTitle)}`;
const searchRes = await fetch(searchUrl, { headers: { 'User-Agent': USER_AGENT } });
const searchHtml = await searchRes.text();
const searchRegex = new RegExp(`href="(https://anihq.cc/watch/[^"]+-episode-${e}-${typeSuffix}/)"`, 'i');
const match = searchHtml.match(searchRegex);
if (match) {
const retryRes = await fetch(match[1], { headers: { 'User-Agent': USER_AGENT } });
html = await retryRes.text();
} else return null;
}
const iframeMatch = html.match(/iframe[^>]+src=["'](https:\/\/(?:[a-z0-9.]+|ellenpoliticalfollow\.com)\/e\/[a-zA-Z0-9]+)["']/i);
if (!iframeMatch) return null;
const result = await extractVoe(iframeMatch[1]);
if (result && result.url) {
const mirrorOrigin = new URL(result.mirror).origin;
return {
url: result.url,
headers: {
'Referer': mirrorOrigin + '/',
'User-Agent': USER_AGENT,
'Origin': mirrorOrigin
}
};
}
} catch (err) {
return null;
}
return null;
}

View File

@@ -0,0 +1,190 @@
import { getTmdbInfo, USER_AGENT, fetchText } from '../utils/helpers.js';
const BASE_URL = "https://anineko.to";
function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
function decodeEntities(str) {
return str.replace(/&(nbsp|amp|quot|lt|gt);/g, (m, e) => ({ "nbsp": " ", "amp": "&", "quot": "\"", "lt": "<", "gt": ">" })[e]).replace(/&#(\d+);/gi, (m, n) => String.fromCharCode(parseInt(n, 10)));
}
async function search(query) {
try {
const html = await fetchText(`${BASE_URL}/browser?keyword=${encodeURIComponent(query)}`, {
headers: { 'User-Agent': USER_AGENT },
signal: AbortSignal.timeout(6000)
});
const results = [];
let idx = 0;
while ((idx = html.indexOf('nv-anime-thumb', idx)) !== -1) {
const startHref = html.lastIndexOf('<a', idx);
if (startHref === -1) { idx += 14; continue; }
const hrefMatch = html.slice(startHref, html.indexOf('>', startHref)).match(/href=["']([^"']+)["']/i);
if (!hrefMatch) { idx += 14; continue; }
const slug = hrefMatch[1].split('/').pop();
const h3Start = html.indexOf('nv-anime-title', idx);
if (h3Start !== -1) {
const contentStart = html.indexOf('>', h3Start) + 1;
const contentEnd = html.indexOf('<', contentStart);
if (contentEnd !== -1) results.push({ slug, text: html.slice(contentStart, contentEnd).trim() });
}
idx += 14;
}
if (results.length === 0) {
const linkMatches = [...html.matchAll(/href=["']\/watch\/([^"']+)["']/gi)];
for (const match of linkMatches) {
if (!results.find(r => r.slug === match[1])) {
const linkIdx = html.indexOf(match[0]);
const titleStart = html.lastIndexOf('>', linkIdx);
const titleEnd = html.indexOf('<', linkIdx);
if (titleStart !== -1 && titleEnd !== -1 && titleEnd > linkIdx) {
const title = html.slice(titleStart + 1, Math.min(titleEnd, linkIdx + 200)).trim();
if (title.length > 2 && title.length < 100) {
results.push({ slug: match[1], text: title });
}
}
}
}
}
return results;
} catch { return []; }
}
async function extractHls(embedUrl) {
try {
const html = await fetchText(embedUrl, {
headers: {
'User-Agent': USER_AGENT,
Referer: `${BASE_URL}/`
},
signal: AbortSignal.timeout(6000)
});
const m = html.match(/const\s+src\s*=\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) ||
html.match(/file\s*:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) ||
html.match(/["'](https?:\/\/[^"']+\/master\.m3u8[^"']*)["']/i) ||
html.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) ||
html.match(/src:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i);
if (m) return decodeEntities(m[1]);
} catch { }
return null;
}
export async function getStream({ id, s, e, audio, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const info = await getTmdbInfo(tmdbApiKey, id, s ? 'tv' : 'movie', s);
if (!info || !info.isAnime) return null;
let seriesSlug = null;
for (const title of info.titles) {
const potentialSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
if (!potentialSlug) continue;
try {
const checkHtml = await fetchText(`${BASE_URL}/watch/${potentialSlug}/ep-${e || 1}`, {
headers: { 'User-Agent': USER_AGENT, Referer: `${BASE_URL}/watch/${potentialSlug}` },
signal: AbortSignal.timeout(4000)
});
if (checkHtml && checkHtml.includes('nv-watch-page')) {
seriesSlug = potentialSlug;
break;
}
} catch {}
}
if (!seriesSlug) {
for (const title of info.titles) {
const results = await search(title);
const targetClean = cleanTitle(title);
const exactSlugMatch = results.find(r => {
const slugClean = cleanTitle(r.slug);
return slugClean === targetClean || r.slug === targetClean;
});
if (exactSlugMatch) {
seriesSlug = exactSlugMatch.slug;
break;
}
const sortedResults = [...results].sort((a, b) => a.slug.length - b.slug.length);
for (const r of sortedResults) {
const resultClean = cleanTitle(r.text);
const isExactMatch = resultClean === targetClean;
const isCloseMatch = r.text.toLowerCase().includes(title.toLowerCase());
if (isExactMatch || isCloseMatch) {
seriesSlug = r.slug;
break;
}
}
if (seriesSlug) break;
}
}
if (!seriesSlug && info.titles.length) {
const fSearch = await search(info.titles[0]);
const sortedResults = [...fSearch].sort((a, b) => a.slug.length - b.slug.length);
if (sortedResults.length) seriesSlug = sortedResults[0].slug;
}
if (!seriesSlug) return null;
const watchHtml = await fetchText(`${BASE_URL}/watch/${seriesSlug}/ep-${e || 1}`, {
headers: {
'User-Agent': USER_AGENT,
Referer: `${BASE_URL}/watch/${seriesSlug}`
},
signal: AbortSignal.timeout(6000)
});
const byAudio = { sub: [], dub: [] };
const iframeMatch = watchHtml.match(/<iframe[^>]*src=["']([^"']+)["'][^>]*>/i);
if (iframeMatch) {
const iframeSrc = decodeEntities(iframeMatch[1]);
const hls = await extractHls(iframeSrc);
if (hls) {
const audioType = audio === 'dub' ? 'dub' : 'sub';
byAudio[audioType].push(iframeSrc);
}
}
if (byAudio.sub.length === 0 && byAudio.dub.length === 0) {
const tabMatches = [...watchHtml.matchAll(/<button[^>]*class=["'][^"']*nv-server-tab[^"']*["'][^>]*data-id=["']([^"']+)["'][^>]*>/gi)];
const tabMap = {};
for (const match of tabMatches) {
const tabId = match[1].toLowerCase();
const tabClassMatch = match[0].match(/class=["'][^"']*(tab_\d+)[^"']/i);
if (tabClassMatch) {
const tabClass = tabClassMatch[1];
tabMap[tabClass] = tabId.includes('dub') ? 'dub' : 'sub';
}
}
const btnRegex = /<button\s+[^>]*class=["'][^"']*server-video[^"']*["'][^>]*>/gi;
const btnMatches = [...watchHtml.matchAll(btnRegex)];
for (const match of btnMatches) {
const btnHtml = match[0];
const videoMatch = btnHtml.match(/data-video=["']([^"']+)["']/i);
const tabMatch = btnHtml.match(/data-tab=["']([^"']+)["']/i);
if (videoMatch && tabMatch) {
const videoUrl = decodeEntities(videoMatch[1]);
const tabClass = tabMatch[1];
const audioType = tabMap[tabClass] || 'sub';
byAudio[audioType].push(videoUrl);
}
}
}
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
const allUrls = [];
for (const aud of audiosToTry) {
for (const embed of byAudio[aud]) {
const hls = await extractHls(embed);
if (hls) allUrls.push({ url: hls, type: "hls", audio: aud, server: "AniNeko", skipProxy: false });
}
if (allUrls.length) break;
}
return allUrls.length ? { allUrls } : null;
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
}

View File

@@ -0,0 +1,202 @@
import { USER_AGENT } from '../utils/helpers.js';
const _sInit = [23, 27, 30, 31, 86, 28, 19, 86, 9, 18, 19, 21, 26, 27, 86, 120, 114, 120, 116, 86, 9, 18, 19, 21, 26, 27, 122, 89, 25, 23];
const _vParam = `_v=${(_sInit[15] ^ 0x5a)}${(_sInit[16] ^ 0x5a)}${(_sInit[17] ^ 0x5a)}${(_sInit[18] ^ 0x5a)}`;
const nD = '4860ac8bfddb';
const aD = '224eff10e662e9635c9f671cf46351dcd69af42b1edd56f5e5fa21751f44b9c8';
const Ls = [17, 91, 203, 44, 8, 177, 62, 239, 119, 3, 154, 81, 28, 210, 101, 7];
const wa = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
const SERVERS = [
{ id: 'NIGHT', endpoint: '/server/night' },
{ id: 'EMP', endpoint: '/server/emp' },
{ id: 'MAIN', endpoint: '/server/vidsrc' }
];
const CORS_PROXY = 'https://eve.pstream.cfd';
let cachedToken = null;
let tokenExpiry = 0;
function ab(e) {
let t = e >>> 0;
t ^= t >>> 16;
t = Math.imul(t, 2146121005) >>> 0;
t ^= t >>> 15;
t = Math.imul(t, 2221713035) >>> 0;
return (t ^ (t >>> 16)) >>> 0;
}
function sD(e) {
const t = new TextEncoder().encode(aD);
const r = Math.max(32, Math.min(128, e + 17));
const n = new Uint8Array(r);
let a = 2166136261;
for (let s = 0; s < r; s += 1) {
a ^= t[s % t.length] ?? s;
a = ab((a + Ls[s % Ls.length] + ((2654435761 * s) >>> 0)) >>> 0);
n[s] = a & 255;
}
return n;
}
function iD(e) {
let t = '';
for (let r = 0; r < e.length; r += 3) {
const n = e[r];
const a = e[r + 1];
const s = e[r + 2];
t += wa[n >>> 2];
t += wa[((3 & n) << 4) | ((a ?? 0) >>> 4)];
if (a === undefined) break;
t += wa[((15 & a) << 2) | ((s ?? 0) >>> 6)];
if (s === undefined) break;
t += wa[63 & s];
}
return t;
}
function generateDirectHlsUrl(id, s, e) {
const isTv = s != null && e != null;
const tmdbId = Math.floor(Number(id));
const season = isTv ? Math.floor(Number(s || 1)) : 0;
const episode = isTv ? Math.floor(Number(e || 1)) : 0;
const str = `${nD}:${isTv ? 's' : 'm'}:${tmdbId}:${season}:${episode}`;
const a = new TextEncoder().encode(str);
const sArr = sD(a.length);
const i = new Uint8Array(a.length + 2);
i[0] = a.length & 255;
i[1] = (a.length >>> 8) & 255;
let o = (2654435769 ^ a.length) >>> 0;
for (let l = 0; l < a.length; l += 1) {
o = ab((o + sArr[l % sArr.length] + Ls[l % Ls.length] + l) >>> 0);
i[l + 2] = (a[l] ^ (255 & o)) ^ sArr[(7 * l + 3) % sArr.length];
}
return `https://glendale-plumbing.com/c/v1/${iD(i)}/master.m3u8`;
}
async function fetchInternalToken() {
if (cachedToken && Date.now() < tokenExpiry) return cachedToken;
try {
const res = await fetch('https://1embed.cc/api/token', {
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://1embed.cc/',
'Accept': 'application/json, text/plain, */*'
},
signal: AbortSignal.timeout(4000)
});
if (res.ok) {
const data = await res.json();
if (data?.token) {
cachedToken = data.token;
tokenExpiry = Date.now() + 1800 * 1000;
return cachedToken;
}
}
} catch { }
try {
const proxiedUrl = `${CORS_PROXY}/?destination=${encodeURIComponent('https://1embed.cc/api/token')}`;
const res = await fetch(proxiedUrl, {
headers: {
'X-Referer': 'https://1embed.cc/',
'X-User-Agent': USER_AGENT
},
signal: AbortSignal.timeout(4000)
});
if (res.ok) {
const data = await res.json();
if (data?.token) {
cachedToken = data.token;
tokenExpiry = Date.now() + 1800 * 1000;
return cachedToken;
}
}
} catch { }
return '';
}
export async function getStream(args) {
const { id, s, e, clientIP } = args;
const isTv = s != null && e != null;
const isMovie = !isTv;
const allUrls = [];
const directHlsUrl = generateDirectHlsUrl(id, s, e);
const masterUrlWithMark = `${directHlsUrl}?${_vParam}`;
allUrls.push({
url: masterUrlWithMark,
server: 'Bcine (Direct Master)',
quality: 'Auto',
type: 'hls',
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://bcine.ru/',
'Origin': 'https://bcine.ru',
...(clientIP && { 'X-Forwarded-For': clientIP })
}
});
const token = await fetchInternalToken();
await Promise.allSettled(
SERVERS.map(async (server) => {
const query = isTv
? `id=${id}?type=tv&s=${s || 1}&e=${e || 1}`
: `id=${id}?type=movie`;
const stParam = token ? `&_st=${token}` : '';
const targetUrl = `https://1embed.cc${server.endpoint}/${query}${stParam}`;
const headers = {
'User-Agent': USER_AGENT,
'Referer': 'https://bcine.ru/',
'Accept': 'application/json, text/plain, */*',
...(token && { 'X-Stream-Token': token }),
...(clientIP && { 'X-Forwarded-For': clientIP })
};
let res = await fetch(targetUrl, { headers, signal: AbortSignal.timeout(5000) }).catch(() => null);
if (!res || !res.ok) {
const proxiedUrl = `${CORS_PROXY}/?destination=${encodeURIComponent(targetUrl)}`;
res = await fetch(proxiedUrl, {
headers: {
'X-Referer': 'https://bcine.ru/',
'X-User-Agent': USER_AGENT
},
signal: AbortSignal.timeout(5000)
}).catch(() => null);
}
if (res && res.ok) {
try {
const data = await res.json();
if (data && data.success !== false) {
const streamUrl = data.streams?.proxy_m3u8 || data.streams?.raw_m3u8 || data.streams?.m3u8 || data.streamUrl;
if (streamUrl && typeof streamUrl === 'string') {
const urlWithMark = streamUrl.includes('?') ? `${streamUrl}&${_vParam}` : `${streamUrl}?${_vParam}`;
allUrls.push({
url: urlWithMark,
server: `Bcine (${data.selectedSource || data.provider || server.id})`,
quality: 'Auto',
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://bcine.ru/',
'Origin': 'https://bcine.ru'
}
});
}
}
} catch { }
}
})
);
return allUrls.length ? { allUrls } : null;
}

View File

@@ -0,0 +1,150 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://dulo.mov';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function resolveDetails(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}`, {
signal: AbortSignal.timeout(6000),
});
if (!res.ok) return null;
const data = await res.json();
const title = isTv ? data.name : data.title;
const releaseDate = isTv ? data.first_air_date : data.release_date;
const year = releaseDate ? releaseDate.split('-')[0] : '';
const runtime = isTv ? (data.episode_run_time?.[0] || 24) : (data.runtime || 90);
return { title, year, runtime };
} catch {
return null;
}
}
async function fetchSource(url, headers) {
try {
const res = await fetch(url, {
headers,
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
export async function getStream({ id, s, e, sdk }) {
const isTv = Boolean(s && e);
const mediaType = isTv ? 'tv' : 'movie';
const ua = getUA();
const details = await resolveDetails(sdk, id, isTv);
const title = details?.title || '';
const year = details?.year || '';
const runtime = details?.runtime || '';
const referer = isTv
? `${BASE_URL}/watch/tv/${id}/${s}/${e}`
: `${BASE_URL}/watch/movie/${id}`;
const defaultHeaders = {
'User-Agent': ua,
Referer: referer,
Origin: BASE_URL,
Accept: '*/*',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
};
const baseParams = new URLSearchParams({
tmdbId: String(id),
type: mediaType,
});
const additionalParams = new URLSearchParams({
tmdbId: String(id),
type: mediaType,
progressive: 'true',
});
if (title) additionalParams.set('title', title);
if (year) additionalParams.set('year', String(year));
if (runtime) additionalParams.set('runtime', String(runtime));
if (isTv) {
baseParams.set('season', String(s));
baseParams.set('episode', String(e));
additionalParams.set('season', String(s));
additionalParams.set('episode', String(e));
}
const endpoints = [
`${BASE_URL}/api/sources?${baseParams.toString()}`,
`${BASE_URL}/api/sources/willow?${baseParams.toString()}`,
`${BASE_URL}/api/sources/additional?${additionalParams.toString()}`,
];
const seenUrls = new Set();
const allUrls = [];
let pendingUrls = [...endpoints];
const maxPollTime = Date.now() + 16000;
while (pendingUrls.length > 0 && Date.now() < maxPollTime) {
const results = await Promise.all(
pendingUrls.map((url) => fetchSource(url, defaultHeaders))
);
const nextPending = [];
results.forEach((data, index) => {
if (!data) return;
if (Array.isArray(data.sources) && data.sources.length > 0) {
for (const src of data.sources) {
let streamUrl = String(src?.url || '').trim();
if (!streamUrl) continue;
if (streamUrl.startsWith('/')) {
streamUrl = `${BASE_URL}${streamUrl}`;
}
if (!streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue;
seenUrls.add(streamUrl);
allUrls.push({
url: streamUrl,
label: src.label || 'Dulo',
headers: {
'User-Agent': ua,
Referer: referer,
},
captions: Array.isArray(src.captions) ? src.captions : [],
});
}
}
if (data.pending) {
nextPending.push(pendingUrls[index]);
}
});
pendingUrls = nextPending;
if (pendingUrls.length > 0) {
await sleep(1500);
}
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
}

View File

@@ -0,0 +1,118 @@
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
const BASE_URL = 'https://flaxmovies.xyz';
async function getWorkerUrls(embedUrl) {
const foundUrls = [];
try {
const res = await fetch(embedUrl, {
headers: {
'User-Agent': USER_AGENT,
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL
},
signal: AbortSignal.timeout(8000)
});
if (res.ok) {
const html = await res.text();
const directMatch = html.match(/WORKER_URL\s*[:=]\s*["'](https?:\/\/[^"']+)["']/i);
if (directMatch && directMatch[1]) {
foundUrls.push(directMatch[1].replace(/\/+$/, ''));
}
const scriptMatches = [...html.matchAll(/<script[^>]+src=["']([^"']+)["']/gi)];
for (const sm of scriptMatches) {
let scriptUrl = sm[1];
if (scriptUrl.startsWith('//')) scriptUrl = `https:${scriptUrl}`;
else if (scriptUrl.startsWith('/')) scriptUrl = `${BASE_URL}${scriptUrl}`;
else if (!scriptUrl.startsWith('http')) scriptUrl = `${BASE_URL}/${scriptUrl}`;
if (scriptUrl.includes('jsdelivr') || scriptUrl.includes('google') || scriptUrl.includes('cloudflare')) continue;
try {
const sRes = await fetch(scriptUrl, {
headers: { 'User-Agent': USER_AGENT, 'Referer': embedUrl },
signal: AbortSignal.timeout(6000)
});
if (sRes.ok) {
const sCode = await sRes.text();
const m = sCode.match(/WORKER_URL\s*[:=]\s*["'](https?:\/\/[^"']+)["']/i);
if (m && m[1]) {
const wUrl = m[1].replace(/\/+$/, '');
if (!foundUrls.includes(wUrl)) foundUrls.push(wUrl);
}
}
} catch { }
}
}
} catch { }
const defaults = ['https://freakyniki.elaxo.lol', 'https://vidlove.nabilekson.workers.dev'];
for (const d of defaults) {
if (!foundUrls.includes(d)) foundUrls.push(d);
}
return foundUrls;
}
export async function getStream(args) {
const { id, s, e, server } = args;
const isTv = s != null && e != null;
const embedUrl = isTv
? `${BASE_URL}/embed/tv/${id}/${s}/${e}`
: `${BASE_URL}/embed/movie/${id}`;
const workerUrls = await getWorkerUrls(embedUrl);
const query = isTv
? `tmdb_id=${id}&tmdbId=${id}&season=${s}&episode=${e}`
: `tmdb_id=${id}&tmdbId=${id}`;
for (const worker of workerUrls) {
try {
const url = `${worker}/?${query}`;
const data = await fetchJson(url, {
headers: {
'User-Agent': USER_AGENT,
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL,
'Accept': 'application/json, text/plain, */*'
},
signal: AbortSignal.timeout(12000)
});
if (data && Array.isArray(data.streams) && data.streams.length > 0) {
let allUrls = data.streams
.filter(st => st && typeof st.url === 'string')
.map(st => {
const isHls = (st.format || '').toLowerCase().includes('hls') || st.url.includes('.m3u8');
return {
url: st.url,
server: `FlaxMovies - ${st.provider || 'Default'}`,
quality: st.resolution || 'Auto',
type: isHls ? 'hls' : 'mp4',
headers: {
'User-Agent': USER_AGENT,
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL
}
};
});
if (server && server !== 'all') {
const clean = server.toLowerCase().replace('flaxmovies - ', '');
const filtered = allUrls.filter(u => u.server.toLowerCase().includes(clean));
if (filtered.length) allUrls = filtered;
}
if (allUrls.length) {
return { allUrls };
}
}
} catch { }
}
return null;
}
export async function getSources() {
return ['FlaxMovies - Airflix', 'FlaxMovies - Xpass-VIP 1'];
}

View File

@@ -0,0 +1,48 @@
import { fetchText, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
const ORIGIN = 'https://www3.fsonline.app';
const AJAX_URL = `${ORIGIN}/wp-admin/admin-ajax.php`;
const HEADERS = { 'User-Agent': USER_AGENT, 'Origin': ORIGIN, 'Referer': `${ORIGIN}/` };
export async function getStream({ id, s, e, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const isTv = s != null && e != null;
const info = await getTmdbInfo(tmdbApiKey, id, isTv ? 'tv' : 'movie');
if (!info?.titles?.length) return null;
const searchHtml = await fetchText(`${ORIGIN}/?s=${encodeURIComponent(info.titles[0] + (info.year ? ' ' + info.year : ''))}`, { headers: HEADERS });
const typeFolder = isTv ? 'seriale' : 'film';
const linkMatch = searchHtml.match(new RegExp(`href=["'](https?://www3\\.fsonline\\.app/${typeFolder}/([^"'/]+)/)["']`, 'i'));
if (!linkMatch) return null;
const targetPageUrl = isTv ? `${ORIGIN}/episoade/${linkMatch[2].replace(/-\d{4}$/, '')}-sezonul-${s}-episodul-${e}/` : linkMatch[1];
const pageHtml = await fetchText(targetPageUrl, { headers: HEADERS });
const movieId = (pageHtml.match(/movie-id=['"]([^'"]+)['"]/) || pageHtml.match(/movie-id=([^ >]+)/))?.[1];
if (!movieId) return null;
const ajaxHtml = await fetchText(AJAX_URL, { method: 'POST', headers: { ...HEADERS, 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Referer': targetPageUrl }, body: `action=lazy_player&movieID=${movieId}` });
const results = [];
let idx = 0;
while ((idx = ajaxHtml.indexOf('data-vs="', idx)) !== -1) {
const embedStart = idx + 9;
const embedEnd = ajaxHtml.indexOf('"', embedStart);
const embedUrl = ajaxHtml.slice(embedStart, embedEnd);
const spanStart = ajaxHtml.indexOf('<span>', embedEnd);
const spanEnd = ajaxHtml.indexOf('</span>', spanStart);
const serverLabel = ajaxHtml.slice(spanStart + 6, spanEnd).trim().toLowerCase();
if (serverLabel.includes('filesun')) {
try {
const rHtml = await fetchText(embedUrl, { headers: { 'Referer': ORIGIN, 'User-Agent': USER_AGENT } });
const m3u8Match = rHtml.match(/file:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/) || rHtml.match(/["']?file["']?:\s*["'](https?:\/\/[^"']+)["']/);
if (m3u8Match) results.push({ url: m3u8Match[1].replace(/\\\//g, '/'), server: 'FSOnline - FileSuN', headers: { 'Referer': embedUrl, 'Origin': 'https://player.fsonline.app', 'User-Agent': USER_AGENT } });
} catch { }
}
idx = spanEnd;
}
return results.length ? { allUrls: results } : null;
} catch { return null; }
}
export async function getSources(args) {
const res = await getStream(args);
return res?.allUrls ? [...new Set(res.allUrls.map(u => u.server))] : [];
}

View File

@@ -0,0 +1,60 @@
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
export const SKIP_VERIFY = true;
export const MULTI_URL = false;
const ENC_API = "https://enc-dec.app/api";
const BASE = "https://kisskh.do";
const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' };
function cleanTitle(t) { return t ? t.toLowerCase().replace(/\(\d{4}\)/g, '').replace(/[^a-z0-9]/g, '') : ''; }
async function encKey(episodeId, type) {
try {
const data = await fetchJson(`${ENC_API}/enc-kisskh?text=${episodeId}&type=${type}`, { signal: AbortSignal.timeout(6000) });
return data?.status === 200 ? data.result : null;
} catch { return null; }
}
export async function getStream({ id, s, e }) {
try {
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
if (!info) return null;
let drama = null, bestScore = -1;
for (const title of (info.titles.length > 1 ? [...info.titles].reverse() : info.titles)) {
const results = await fetchJson(`${BASE}/api/DramaList/Search?q=${encodeURIComponent(title)}`, { headers: HEADERS, signal: AbortSignal.timeout(6000) }).catch(() => []);
const targetClean = cleanTitle(title);
for (const item of results) {
const parts = (item.title || '').split(/\s*-\s*/).map(cleanTitle);
let score = 0;
if (parts.some(p => p === targetClean)) score += 5;
else if (parts.some(p => p.includes(targetClean) || targetClean.includes(p))) score += 3;
else continue;
if (score > bestScore) { bestScore = score; drama = item; }
}
if (drama && bestScore >= 5) break;
}
if (!drama) return null;
const detail = await fetchJson(`${BASE}/api/DramaList/Drama/${drama.id}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
const episodeId = detail?.episodes?.find(ep => Math.floor(ep.number) === Number(e || 1))?.id;
if (!episodeId) return null;
const vidKey = await encKey(episodeId, 'vid');
if (!vidKey) return null;
const videoData = await fetchJson(`${BASE}/api/DramaList/Episode/${episodeId}.png?err=false&ts=&time=&kkey=${vidKey}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
if (!videoData?.Video) return null;
let subtitles = [];
const subKey = await encKey(episodeId, 'sub');
if (subKey) {
try {
const subList = await fetchJson(`${BASE}/api/Sub/${episodeId}?kkey=${subKey}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
if (Array.isArray(subList)) subtitles = subList.map(sub => ({ url: sub.src, label: sub.label, lang: sub.land, default: !!sub.default }));
} catch { }
}
return { allUrls: [{ url: videoData.Video, type: videoData.Video.includes('.m3u8') ? 'hls' : 'mp4', audio: 'sub', server: 'KissKH', skipProxy: false, subtitles }] };
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
}

View File

@@ -0,0 +1,43 @@
import { getTmdbInfo } from '../utils/helpers.js';
export async function getStream(args) {
const { id, s, e, tmdbApiKey } = args;
const type = (s != null && e != null) ? 'tv' : 'movie';
try {
const tmdbInfo = await getTmdbInfo(tmdbApiKey, id, type, s);
const name = tmdbInfo.titles[0] || '';
if (!name) return null;
if (type === 'tv') {
return null;
} else {
const url = `https://lmscript.xyz/v1/movies?filters[q]=${encodeURIComponent(name)}&expand=streams`;
const response = await fetch(url);
const data = await response.json();
const movie = data.items?.find(item => `${item.tmdb_prefix}` === `${id}` || `${item.tmdb_id}` === `${id}`);
if (!movie) return null;
const streams = movie.streams || {};
const SOURCE_ORDER = ["1080p", "720p", "480p", "360p"];
const sortedStreams = Object.keys(streams).sort((a, b) => SOURCE_ORDER.indexOf(a) - SOURCE_ORDER.indexOf(b));
if (!sortedStreams.length) return null;
const allUrls = sortedStreams.map(quality => {
const streamUrl = streams[quality];
return {
url: streamUrl,
quality,
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
headers: {}
};
});
return { allUrls };
}
} catch (err) {
return null;
}
}

View File

@@ -0,0 +1,80 @@
import { USER_AGENT, fetchText, fetchJson } from '../utils/helpers.js';
const TMDB_BASE = 'https://api.themoviedb.org/3';
const LM_DOMAINS = ['https://www.lookmovie2.to', 'https://lookmovie2.to', 'https://lookmovie.foundation'];
const HEADERS_BASE = { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9' };
async function searchLookMovie(type, title, year, clientIP) {
for (const base of LM_DOMAINS) {
try {
const headers = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
const data = await fetchJson(`${base}/api/v1/${type}/do-search/?q=${encodeURIComponent(title)}`, { headers, signal: AbortSignal.timeout(4000) });
const results = data?.result;
if (results?.length) {
const match = results.find(r => String(r.year) === String(year)) ?? results.find(r => r.title?.toLowerCase() === title.toLowerCase()) ?? results[0];
if (match) return { match, base };
}
} catch { }
}
return null;
}
const STORAGE_RE = /window\[['"](?:movie|show)_storage['"]\]\s*=\s*\{([^}]+)\}/s;
const ID_MOVIE_RE = /['"]?(?:id_movie|movieId)['"]?\s*[:=]\s*['"]?(\d+)['"]?/i;
const SEASONS_RE = /seasons\s*:\s*(\[[\s\S]+?\])\s*[,}]/;
function getEpisodeIdFast(html, s, e) {
const sMatch = html.match(STORAGE_RE);
if (sMatch) {
const block = sMatch[1], sm = block.match(SEASONS_RE);
if (sm) {
try {
const seasons = JSON.parse(sm[1]);
const season = seasons.find(x => String(x?.season ?? x?.meta?.season) === String(s));
if (season) {
const ep = Array.isArray(season.episodes) ? season.episodes.find(x => String(x.episode) === String(e)) : (season.episodes?.[String(e)] || Object.values(season.episodes || {}).find(x => String(x.episode) === String(e)));
if (ep) return String(ep.id_episode ?? ep.id);
}
} catch { }
}
}
const parts = html.split(/id_episode['"]?\s*[:=]\s*['"]?(\d+)['"]?/i);
for (let i = 1; i < parts.length; i += 2) {
const context = parts[i - 1].slice(-300) + parts[i + 1].slice(0, 300);
if (context.match(new RegExp(`['"]?episode['"]?\\s*[:=]\\s*['"]?${e}['"]?`, 'i')) && context.match(new RegExp(`['"]?season['"]?\\s*[:=]\\s*['"]?${s}['"]?`, 'i'))) return parts[i];
}
const am = html.match(new RegExp(`data-season=["']${s}["'][^>]*?data-episode=["']${e}["'][^>]*?data-id=["'](\\d+)["']`, 'i')) || html.match(new RegExp(`data-episode=["']${e}["'][^>]*?data-season=["']${s}["'][^>]*?data-id=["'](\\d+)["']`, 'i'));
return am ? am[1] : null;
}
export async function getStream({ id, s, e, sdk, clientIP }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const tmdbKey = tmdbApiKey;
if (!tmdbKey) return null;
const isTV = s != null && e != null;
const typeStr = isTV ? 'shows' : 'movies';
const tmdbData = await fetchJson(`${TMDB_BASE}/${isTV ? 'tv' : 'movie'}/${id}?api_key=${tmdbKey}`, { signal: AbortSignal.timeout(3000) });
const title = tmdbData?.title || tmdbData?.name;
const year = (tmdbData?.first_air_date || tmdbData?.release_date || '').slice(0, 4);
if (!title) return null;
const searchRes = await searchLookMovie(typeStr, title, year, clientIP);
if (!searchRes?.match?.slug) return null;
const { match, base } = searchRes;
const headers = { ...HEADERS_BASE, 'Accept': 'text/html', 'Referer': `${base}/`, ...(clientIP && { 'X-Forwarded-For': clientIP }) };
const html = await fetchText(`${base}/${typeStr}/play/${match.slug}`, { headers, signal: AbortSignal.timeout(8000) });
const storageMatch = html.match(STORAGE_RE);
if (!storageMatch) return null;
const hashMatch = storageMatch[1].match(/hash\s*:\s*['"]([^'"]+)['"]/);
const expiresMatch = storageMatch[1].match(/expires\s*:\s*(\d+)/);
if (!hashMatch || !expiresMatch) return null;
const streamId = isTV ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]);
if (!streamId) return null;
const accessHeaders = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
const data = await fetchJson(`${base}/api/v1/security/${isTV ? 'episode' : 'movie'}-access?id_${isTV ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) });
const streams = data?.streams ?? data?.result?.streams ?? data?.data?.streams ?? data;
const allUrls = Object.entries(streams || {}).filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')).map(([quality, url]) => ({ url, quality }));
return allUrls.length ? { allUrls } : null;
} catch { return null; }
}

View File

@@ -0,0 +1,117 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://movienig.ht';
const PRIORITY_SERVERS = [
{ id: 'dallas', label: 'Dallas 4K' },
{ id: 'austin', label: 'Austin' },
{ id: 'helena', label: 'Helena' },
{ id: 'seattle', label: 'Seattle 4K' },
{ id: 'vixsrc-1', label: 'Newport Beach' },
{ id: 'tucson', label: 'Tucson' },
{ id: 'salem', label: 'Salem' },
];
async function resolveDetails(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();
const title = isTv ? data.name : data.title;
const releaseDate = isTv ? data.first_air_date : data.release_date;
const year = releaseDate ? parseInt(releaseDate.split('-')[0], 10) : null;
const imdbId = data.imdb_id || data.external_ids?.imdb_id || null;
return { title, year, imdbId };
} catch {
return null;
}
}
export async function getStream({ id, s, e, sdk }) {
const isTv = Boolean(s && e);
const ua = getUA();
const details = await resolveDetails(sdk, id, isTv);
const title = details?.title || String(id);
const year = details?.year;
const imdbId = details?.imdbId;
const encTitle = encodeURIComponent(title);
const yearQuery = year ? `&year=${year}` : '';
const imdbQuery = imdbId ? `&imdbId=${imdbId}` : '';
const seenUrls = new Set();
const allUrls = [];
const defaultHeaders = {
'User-Agent': ua,
Referer: `${BASE_URL}/`,
Origin: BASE_URL,
Accept: 'text/event-stream',
};
const tasks = PRIORITY_SERVERS.map(async (server) => {
try {
const url = isTv
? `${BASE_URL}/api/stream/v1/tv/${id}/${s}/${e}?title=${encTitle}${yearQuery}${imdbQuery}&server=${server.id}&only=1`
: `${BASE_URL}/api/stream/v1/movie/${id}?title=${encTitle}${yearQuery}${imdbQuery}&server=${server.id}&only=1`;
const res = await fetch(url, {
headers: defaultHeaders,
signal: AbortSignal.timeout(6000),
});
if (!res.ok) return;
const bodyStr = await res.text();
if (bodyStr.includes('event: done')) {
const doneIdx = bodyStr.indexOf('event: done');
const dataIdx = bodyStr.indexOf('data: ', doneIdx);
if (dataIdx !== -1) {
const jsonStart = dataIdx + 6;
const jsonEnd = bodyStr.indexOf('\n', jsonStart);
const jsonText = (jsonEnd !== -1 ? bodyStr.substring(jsonStart, jsonEnd) : bodyStr.substring(jsonStart)).trim();
const data = JSON.parse(jsonText);
if (Array.isArray(data?.sources)) {
for (const src of data.sources) {
if (!src || typeof src !== 'object') continue;
const rawUrl = String(src.url || '').trim();
if (!rawUrl || seenUrls.has(rawUrl)) continue;
seenUrls.add(rawUrl);
const quality = src.quality ? String(src.quality) : 'Auto';
const titleQuality = quality !== 'Auto' ? ` (${quality})` : '';
allUrls.push({
url: rawUrl,
label: `MovieNight ${server.label}${titleQuality}`,
headers: {
'User-Agent': ua,
Referer: `${BASE_URL}/`,
},
});
}
}
}
}
} catch { }
});
await Promise.all(tasks);
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
}

View File

@@ -0,0 +1,282 @@
import { getUA } from '../utils/helpers.js';
const API_BASE = 'https://api.wecollege.net';
const REFERER = 'https://www.movy.bz/';
const ORIGIN = 'https://www.movy.bz';
const MAGIC = [109, 118, 109, 49];
const SERVERS = [
{ endpoint: 'miami', name: 'Miami', note: 'Original audio (Up to 4K)' },
{ endpoint: 'seattle', name: 'Seattle', note: 'Original audio' },
{ endpoint: 'denver', name: 'Denver', note: 'Original audio' },
{ endpoint: 'chicago', name: 'Chicago', note: 'Original audio' },
{ endpoint: 'dallas', name: 'Dallas', note: 'Original audio' },
{ endpoint: 'atlanta', name: 'Atlanta', note: 'Original audio' },
{ endpoint: 'houston', name: 'Houston', note: 'Original audio' },
{ endpoint: 'austin', name: 'Austin', note: 'Original audio' },
{ endpoint: 'boston', name: 'Boston', note: 'Original audio' },
{ endpoint: 'munich', name: 'Munich', note: 'German audio', extra: 'language=german' },
{ endpoint: 'berlin', name: 'Berlin', note: 'German audio' },
{ endpoint: 'paris', name: 'Paris', note: 'French audio' },
{ endpoint: 'delhi', name: 'Delhi', note: 'Hindi audio' },
{ endpoint: 'cancun', name: 'Cancun', note: 'Spanish audio' },
];
const seedCache = new Map();
function _l(e) {
let v = e >>> 0;
v = (v ^ (v >>> 16)) >>> 0;
v = Math.imul(v, 0x85ebca6b) >>> 0;
v = (v ^ (v >>> 13)) >>> 0;
v = Math.imul(v, 0xc2b2ae35) >>> 0;
return (v ^ (v >>> 16)) >>> 0;
}
function _u(e, t) {
const shift = t & 31;
if (shift === 0) return e >>> 0;
return (((e << shift) >>> 0) | (e >>> (32 - shift))) >>> 0;
}
function _fnv1a(str) {
let t = 0x811c9dc5 >>> 0;
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
t = Math.imul((t ^ code) >>> 0, 0x1000193) >>> 0;
}
return _l(t);
}
function _initKeyState(seed, tmdbId) {
const s = new Array(61).fill(0);
const isSet = new Array(61).fill(false);
const idVal = ((tmdbId >>> 0) ^ 0x9e3779b9) >>> 0;
let r = _l((_fnv1a(seed) ^ _l(idVal)) >>> 0);
for (let e = 0; e < 8; e++) {
const t = r % 61;
r = _u((r + 0x9e3779b9) >>> 0, 7 + (7 & e));
s[t] = (r ^ _l(r)) >>> 0;
isSet[t] = true;
r = _l((r + t) >>> 0);
}
const acc = _l((0xa5a5a5a5 ^ r) >>> 0);
return { s, isSet, acc };
}
function _nextKeystreamWord(state, t) {
const r = state.s;
let nState = state.acc;
const i = nState % 61;
const d = state.isSet[i] ? r[i] : 0;
const c = Math.imul((t + 1) >>> 0, 0x9e3779b9) >>> 0;
const a = nState;
const sVal = (d ^ c) >>> 0;
const h = state.isSet[i] ? ((a | sVal) >>> 0) : ((a ^ sVal) >>> 0);
const term1 = _u((h + nState) >>> 0, 31 & i);
const term2 = _u(nState, 31 & (i * 7));
nState = _l((((term1 ^ term2) >>> 0) + 0x9e3779b9) >>> 0);
r[i] = nState;
state.isSet[i] = true;
state.acc = nState;
return nState >>> 0;
}
function _generateKeyStream(seed, tmdbId, len) {
const state = _initKeyState(seed, tmdbId);
const out = new Uint8Array(len);
let wordIdx = 0;
let byteIdx = 0;
while (byteIdx < len) {
const word = _nextKeystreamWord(state, wordIdx++);
out[byteIdx++] = word & 0xFF;
if (byteIdx < len) out[byteIdx++] = (word >>> 8) & 0xFF;
if (byteIdx < len) out[byteIdx++] = (word >>> 16) & 0xFF;
if (byteIdx < len) out[byteIdx++] = (word >>> 24) & 0xFF;
}
return out;
}
function decrypt(cipherB64, seed, tmdbId) {
try {
let normalized = cipherB64.replace(/-/g, '+').replace(/_/g, '/');
while (normalized.length % 4 !== 0) normalized += '=';
const cipherBytes = Buffer.from(normalized, 'base64');
if (cipherBytes.length <= MAGIC.length) return null;
const ks = _generateKeyStream(seed, tmdbId, cipherBytes.length);
const decrypted = new Uint8Array(cipherBytes.length);
for (let i = 0; i < cipherBytes.length; i++) {
decrypted[i] = cipherBytes[i] ^ ks[i];
}
for (let k = 0; k < MAGIC.length; k++) {
if (decrypted[k] !== MAGIC[k]) return null;
}
return Buffer.from(decrypted.subarray(MAGIC.length)).toString('utf8');
} catch {
return null;
}
}
async function getSeed(tmdbId, ua) {
const now = Date.now();
const cached = seedCache.get(tmdbId);
if (cached && cached.expiresAt > now + 5000) {
return cached.seed;
}
try {
const res = await fetch(`${API_BASE}/seed?mediaId=${tmdbId}`, {
headers: {
'User-Agent': ua,
Referer: REFERER,
Origin: ORIGIN,
},
signal: AbortSignal.timeout(8000),
});
if (res.ok) {
const data = await res.json();
const seed = data?.seed ? String(data.seed) : '';
const ttlMs = typeof data?.ttlMs === 'number' ? data.ttlMs : 30000;
if (seed) {
seedCache.set(tmdbId, { seed, expiresAt: now + ttlMs });
return seed;
}
}
} catch { }
return null;
}
async function resolveDetails(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();
const title = isTv ? data.name : data.title;
const releaseDate = isTv ? data.first_air_date : data.release_date;
const year = releaseDate ? parseInt(releaseDate.split('-')[0], 10) : null;
const imdbId = data.imdb_id || data.external_ids?.imdb_id || null;
return { title, year, imdbId };
} catch {
return null;
}
}
function formatQuality(raw) {
const lower = String(raw).toLowerCase();
if (lower.includes('2160') || lower.includes('4k')) return '4K';
if (lower.includes('1080')) return '1080p';
if (lower.includes('720')) return '720p';
if (lower.includes('480')) return '480p';
if (lower.includes('360')) return '360p';
if (raw) return String(raw);
return 'Auto';
}
export async function getStream({ id, s, e, sdk }) {
const isTv = Boolean(s && e);
const mediaType = isTv ? 'tv' : 'movie';
const tmdbId = parseInt(id, 10);
const ua = getUA();
const details = await resolveDetails(sdk, id, isTv);
const title = details?.title || String(id);
const year = details?.year;
const imdbId = details?.imdbId;
const seed = await getSeed(tmdbId, ua);
if (!seed) return null;
const params = new URLSearchParams({
title,
mediaType,
tmdbId: String(tmdbId),
enc: '2',
seed,
});
if (year) params.set('year', String(year));
if (isTv) {
if (s) params.set('seasonId', String(s));
if (e) params.set('episodeId', String(e));
}
if (imdbId) params.set('imdbId', imdbId);
const baseQuery = params.toString();
const seenUrls = new Set();
const allUrls = [];
const defaultHeaders = {
'User-Agent': ua,
Referer: REFERER,
Origin: ORIGIN,
};
const tasks = SERVERS.map(async (server) => {
let fullUrl = `${API_BASE}/${server.endpoint}/sources?${baseQuery}`;
if (server.extra) fullUrl += `&${server.extra}`;
try {
const res = await fetch(fullUrl, {
headers: defaultHeaders,
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return;
const encText = (await res.text()).trim();
if (!encText || encText.startsWith('<')) return;
const decJsonStr = decrypt(encText, seed, tmdbId);
if (!decJsonStr) return;
const parsed = JSON.parse(decJsonStr);
if (!Array.isArray(parsed?.sources)) return;
for (const src of parsed.sources) {
if (!src || typeof src !== 'object') continue;
const streamUrl = String(src.url || '').trim();
if (!streamUrl || seenUrls.has(streamUrl)) continue;
seenUrls.add(streamUrl);
const cleanQuality = formatQuality(src.quality || 'Auto');
const isSpecificAudio = server.note.toLowerCase().includes('audio') && !server.note.toLowerCase().includes('original');
const langLabel = isSpecificAudio ? server.note.split(' ')[0] : '';
const label = isSpecificAudio
? `${server.name} (${langLabel}) · ${cleanQuality}`
: `${server.name} · ${cleanQuality}`;
allUrls.push({
url: streamUrl,
label,
headers: {
'User-Agent': ua,
Referer: REFERER,
},
});
}
} catch { }
});
await Promise.all(tasks);
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
}

View File

@@ -0,0 +1,50 @@
import { fetchJson } from '../utils/helpers.js';
// const PENGU_CONFIG = '%7B%22source_4khdhub%22%3A%22on%22%2C%22source_moviebox%22%3A%22on%22%2C%22source_moviesdrives%22%3A%22on%22%2C%22source_vaplayer%22%3A%22on%22%2C%22source_hdghartv%22%3A%22on%22%2C%22res_2160%22%3A%22on%22%2C%22res_1080%22%3A%22on%22%2C%22res_720%22%3A%22on%22%2C%22disable_direct%22%3A%22on%22%7D';
// const BASE_URL = `https://pengu.uk/${PENGU_CONFIG}`;
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
const key = tmdbApiKey;
if (!key) return null;
try {
const url = s != null && e != null
? `https://api.themoviedb.org/3/tv/${tmdbId}/external_ids?api_key=${key}`
: `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${key}`;
const res = await fetchJson(url);
return res?.imdb_id || null;
} catch { return null; }
}
export async function getStream({ id, s, e, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
const manifest = sdk?.penguManifest || null;
if (!tmdbApiKey || !manifest || (manifest && !manifest.endsWith('manifest.json'))) return null;
const BASE_URL = manifest ? manifest.split('/manifest.json')[0] : null;
if (!BASE_URL) return null;
try {
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
if (!imdbId) return null;
const path = s != null && e != null
? `/stream/series/${imdbId}:${s}:${e}.json`
: `/stream/movie/${imdbId}.json`;
const res = await fetchJson(`${BASE_URL}${path}`);
if (!res?.streams?.length) return null;
const allUrls = res.streams.map(stream => {
if (!stream.url) return null;
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
return {
url: stream.url,
server: `Pengu - ${stream.name || 'Server'}`,
headers: Object.keys(headers).length > 0 ? headers : undefined,
skipProxy: stream.behaviorHints?.notWebReady === false
};
}).filter(Boolean);
return allUrls.length ? { allUrls } : null;
} catch {
return null;
}
}

View File

@@ -0,0 +1,68 @@
import { USER_AGENT } from '../utils/helpers.js';
async function safeImport(moduleName) {
try {
return await import(`./${moduleName}.js`);
} catch {
return null;
}
}
export async function getStream(args) {
const { id, s, e, clientIP, sdk } = args;
const isTv = s != null && e != null;
const type = isTv ? 'tv' : 'movie';
const allUrls = [];
const providers = [
{ name: 'CineSu', key: 'cinesu', fnName: 'fetchCineSuStreams' },
{ name: 'ZxcStream', key: 'zxcstream', fnName: 'fetchZxcStreams' },
{ name: 'VidFast', key: 'vidfast', fnName: 'fetchVidFastStreams' },
{ name: '1Embed', key: '1embed', fnName: 'fetchOneEmbedStreams' },
];
await Promise.all(
providers.map(async ({ name, key, fnName }) => {
try {
let streamResult = null;
if (sdk) {
try {
streamResult = await sdk.getStream(key, id, s, e, clientIP);
} catch { }
}
if (!streamResult) {
const mod = await safeImport(key);
if (mod) {
if (typeof mod.getStream === 'function') {
streamResult = await mod.getStream(args);
} else if (typeof mod[fnName] === 'function') {
const raw = await mod[fnName]({ type, id, season: s, episode: e });
if (Array.isArray(raw)) {
streamResult = { allUrls: raw };
}
}
}
}
if (!streamResult) return;
const streams = streamResult.allUrls || (streamResult.url ? [streamResult] : []);
for (const item of streams) {
allUrls.push({
url: item.url,
server: `FlyStream (${item.source || item.server || name})`,
quality: item.quality || 'Auto',
type: item.type || item.format || (item.url?.includes('.m3u8') ? 'hls' : 'mp4'),
headers: item.headers || {
'User-Agent': USER_AGENT,
},
});
}
} catch { }
})
);
return allUrls.length ? { allUrls } : null;
}

View File

@@ -0,0 +1,104 @@
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
const DOMAIN = 'https://purstream.ad';
const API_BASE = 'https://api.purstream.ad/api/v1';
const HEADERS = {
'User-Agent': USER_AGENT,
'Accept': 'application/json, text/plain, */*',
'Referer': `${DOMAIN}/`,
'Origin': DOMAIN,
'X-Requested-With': 'XMLHttpRequest',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-site'
};
export async function getStream({ id, s, e }) {
try {
const isTv = s != null;
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
if (!info?.titles?.length) {
return null;
}
const title = info.titles[0];
const searchUrl = `${API_BASE}/search-bar/search/${encodeURIComponent(title)}`;
const searchData = await fetchJson(searchUrl, {
headers: HEADERS,
signal: AbortSignal.timeout(10000)
});
const items = searchData?.data?.items?.movies?.items || [];
const type = isTv ? 'tv' : 'movie';
const lowerTitle = title.toLowerCase();
let match = items.find(item =>
item.type === type &&
item.title?.toLowerCase() === lowerTitle &&
(!info.year || item.release_date?.startsWith(String(info.year)))
);
if (!match) {
match = items.find(item => item.type === type);
}
if (!match?.id) {
return null;
}
const streamUrl = isTv
? `${API_BASE}/stream/${match.id}/episode?season=${s}&episode=${e}`
: `${API_BASE}/stream/${match.id}`;
const json = await fetchJson(streamUrl, {
headers: HEADERS,
signal: AbortSignal.timeout(10000)
});
const sources = json?.data?.items?.sources;
if (json?.type !== 'success' || !Array.isArray(sources) || !sources.length) {
return null;
}
const chosen = sources.find(src => src.stream_url);
if (!chosen?.stream_url) {
return null;
}
const result = {
url: chosen.stream_url,
headers: {
'User-Agent': USER_AGENT,
'Referer': `${DOMAIN}/`
},
server: 'purstream',
quality: chosen.source_name || 'Auto',
type: chosen.format || 'hls',
skipProxy: true
};
return result;
} catch (err) {
return null;
}
}
export async function getSources(args) {
const stream = await getStream(args);
return stream ? [stream] : [];
}
export const VERIFY_HEADERS = {
...HEADERS
};

View File

@@ -0,0 +1,179 @@
// im being salty
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
const C_ARRAY = [
'4Z7lUo', 'gwIVSMD', 'PLmz2elE2v', 'Z4OFV0', 'SZ6RZq6Zc', 'zhJEFYxrz8', 'FOm7b0', 'axHS3q4KDq', 'o9zuXQ', '4Aebt',
'wgjjWwKKx', 'rY4VIxqSN', 'kfjbnSo', '2DyrFA1M', 'YUixDM9B', 'JQvgEj0', 'mcuFx6JIek', 'eoTKe26gL', 'qaI9EVO1rB', '0xl33btZL',
'1fszuAU', 'a7jnHzst6P', 'wQuJkX', 'cBNhTJlEOf', 'KNcFWhDvgT', 'XipDGjST', 'PCZJlbHoyt', '2AYnMZkqd', 'HIpJh', 'KH0C3iztrG',
'W81hjts92', 'rJhAT', 'NON7LKoMQ', 'NMdY3nsKzI', 't4En5v', 'Qq5cOQ9H', 'Y9nwrp', 'VX5FYVfsf', 'cE5SJG', 'x1vj1',
'HegbLe', 'zJ3nmt4OA', 'gt7rxW57dq', 'clIE9b', 'jyJ9g', 'B5jXjMCSx', 'cOzZBZTV', 'FTXGy', 'Dfh1q1', 'ny9jqZ2POI',
'X2NnMn', 'MBtoyD', 'qz4Ilys7wB', '68lbOMye', '3YUJnmxp', '1fv5Imona', 'PlfvvXD7mA', 'ZarKfHCaPR', 'owORnX', 'dQP1YU',
'dVdkx', 'qgiK0E', 'cx9wQ', '5F9bGa', '7UjkKrp', 'Yvhrj', 'wYXez5Dg3', 'pG4GMU', 'MwMAu', 'rFRD5wlM'
];
const DEFAULT_SERVICES = [
'apex', 'pulse', 'solstice', 'quasar', 'horizon', 'primevids',
'flowcast', 'asiacloud', 'citadel', 'hindicast', 'guru'
];
function generateRiveSecretKey(e) {
if (e === undefined || e === null) return 'rive';
try {
let t, n;
const r = String(e);
if (isNaN(Number(e))) {
const sum = r.split('').reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
t = C_ARRAY[sum % C_ARRAY.length] || Buffer.from(r).toString('base64');
n = Math.floor((sum % r.length) / 2);
} else {
const i = Number(e);
t = C_ARRAY[i % C_ARRAY.length] || Buffer.from(r).toString('base64');
n = Math.floor((i % r.length) / 2);
}
const i = r.slice(0, n) + t + r.slice(n);
const o = (function (str) {
const t = String(str);
let n = 3735928559 ^ t.length;
for (let e = 0; e < t.length; e++) {
const r = t.charCodeAt(e);
const rXor = (131 * e + 89 ^ (r << (e % 5))) & 255;
const r2 = r ^ rXor;
n = (((n << 7) | (n >>> 25)) >>> 0) ^ r2;
const idx = (65535 & n) * 60205;
const o2 = ((n >>> 16) * 60205) << 16;
n = (idx + o2) >>> 0;
n ^= n >>> 11;
}
n ^= n >>> 15;
n = ((65535 & n) * 49842 + (((n >>> 16) * 49842) << 16)) >>> 0;
n ^= n >>> 13;
n = ((65535 & n) * 40503 + (((n >>> 16) * 40503) << 16)) >>> 0;
n ^= n >>> 16;
n = ((65535 & n) * 10196 + (((n >>> 16) * 10196) << 16)) >>> 0;
return ((n ^= n >>> 15)).toString(16).padStart(8, '0');
})(
(function (str) {
const s = String(str);
let t = 0;
for (let n = 0; n < s.length; n++) {
const r = s.charCodeAt(n);
const i = (((t = (r + (t << 6) + (t << 16) - t) >>> 0) << (n % 5)) | (t >>> (32 - (n % 5)))) >>> 0;
t ^= (i ^ ((r << (n % 7)) | (r >>> (8 - (n % 7))))) >>> 0;
t = (t + ((t >>> 11) ^ (t << 3))) >>> 0;
}
t ^= t >>> 15;
t = ((65535 & t) * 49842 + ((((t >>> 16) * 49842) & 65535) << 16)) >>> 0;
t ^= t >>> 13;
t = ((65535 & t) * 40503 + ((((t >>> 16) * 40503) & 65535) << 16)) >>> 0;
return ((t ^= t >>> 16)).toString(16).padStart(8, '0');
})(i)
);
return Buffer.from(o, 'utf-8').toString('base64');
} catch {
return 'topSecret';
}
}
async function fetchRiveServices() {
try {
const secretKey = generateRiveSecretKey();
const url = `https://www.rivestream.app/api/backendfetch?requestID=VideoProviderServices&secretKey=${encodeURIComponent(secretKey)}`;
const res = await fetchJson(url, {
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://www.rivestream.app/',
'Accept': 'application/json, text/plain, */*'
},
signal: AbortSignal.timeout(6000)
});
return Array.isArray(res?.data) && res.data.length ? res.data : DEFAULT_SERVICES;
} catch {
return DEFAULT_SERVICES;
}
}
async function fetchServiceStreams(service, isMovie, id, s, e, secretKey, referer) {
try {
const cb = (service === 'primevids' || service === 'citadel')
? `&cb=${Math.floor(Date.now() / 3e6)}`
: '';
const reqID = isMovie ? 'movieVideoProvider' : 'tvVideoProvider';
const params = isMovie
? `requestID=${reqID}&id=${encodeURIComponent(id)}&service=${service}${cb}&secretKey=${encodeURIComponent(secretKey)}`
: `requestID=${reqID}&id=${encodeURIComponent(id)}&season=${encodeURIComponent(s)}&episode=${encodeURIComponent(e)}&service=${service}${cb}&secretKey=${encodeURIComponent(secretKey)}`;
const url = `https://www.rivestream.app/api/backendfetch?${params}`;
const res = await fetchJson(url, {
headers: {
'User-Agent': USER_AGENT,
'Referer': referer,
'Accept': 'application/json, text/plain, */*'
},
signal: AbortSignal.timeout(10000)
});
if (!res?.data?.sources || !Array.isArray(res.data.sources)) return [];
const streams = [];
for (const item of res.data.sources) {
if (!item || !item.url) continue;
const format = String(item.format || '').toLowerCase();
const urlStr = String(item.url || '').toLowerCase();
const isM3U8 = format === 'hls' || format === 'm3u8' || urlStr.includes('.m3u8') || urlStr.includes('.txt');
streams.push({
url: item.url,
server: `Rive (${item.source || service})`,
quality: String(item.quality || 'Auto'),
type: isM3U8 ? 'hls' : 'mp4',
headers: {
'User-Agent': USER_AGENT,
'Referer': referer
}
});
}
return streams;
} catch {
return [];
}
}
export async function getStream(args) {
const { id, s, e, server } = args;
const isTv = s != null && e != null;
const isMovie = !isTv;
const secretKey = generateRiveSecretKey(id);
const referer = isMovie
? `https://www.rivestream.app/watch?type=movie&id=${id}`
: `https://www.rivestream.app/watch?type=tv&id=${id}&season=${s}&episode=${e}`;
let services = await fetchRiveServices();
if (server && server !== 'all') {
const clean = server.toLowerCase().replace('rive (', '').replace(')', '').replace('rive', '').trim();
services = services.filter(srv => srv.toLowerCase().includes(clean));
if (!services.length) services = await fetchRiveServices();
}
const settled = await Promise.allSettled(
services.map(srv => fetchServiceStreams(srv, isMovie, id, s, e, secretKey, referer))
);
const allUrls = [];
for (const r of settled) {
if (r.status === 'fulfilled' && Array.isArray(r.value)) {
allUrls.push(...r.value);
}
}
return allUrls.length ? { allUrls } : null;
}
export async function getSources() {
const services = await fetchRiveServices();
return services.map(s => `Rive (${s})`);
}

View File

@@ -0,0 +1,129 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://streamaggregator.in';
async function resolveDetails(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}`, {
signal: AbortSignal.timeout(6000),
});
if (!res.ok) return null;
const data = await res.json();
const title = isTv ? data.name : data.title;
const releaseDate = isTv ? data.first_air_date : data.release_date;
const year = releaseDate ? releaseDate.split('-')[0] : null;
return { title, year };
} catch {
return null;
}
}
export async function getStream({ id, s, e, sdk }) {
const isTv = Boolean(s && e);
const mediaType = isTv ? 'tv' : 'movie';
const ua = getUA();
const details = await resolveDetails(sdk, id, isTv);
const title = details?.title || String(id);
const year = details?.year || '';
const formattedTitle = encodeURIComponent(title).replaceAll('%20', '+');
const params = new URLSearchParams({
type: mediaType,
stream: 'true',
});
if (year) params.set('year', String(year));
if (isTv) {
params.set('season', String(s));
params.set('episode', String(e));
}
const scrapeUrl = `${BASE_URL}/api/scrape?title=${formattedTitle}&${params.toString()}`;
const res = await fetch(scrapeUrl, {
headers: {
'User-Agent': ua,
Referer: `${BASE_URL}/`,
Origin: BASE_URL,
Accept: 'text/event-stream, application/x-ndjson, text/plain, */*',
},
signal: AbortSignal.timeout(90000),
});
if (!res.ok) return null;
const lines = [];
if (res.body?.getReader) {
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split('\n');
buffer = parts.pop() || '';
for (const part of parts) {
const trimmed = part.trim();
if (trimmed) lines.push(trimmed);
}
}
} catch { }
if (buffer.trim()) lines.push(buffer.trim());
} else {
const text = await res.text();
for (const line of text.split('\n')) {
const trimmed = line.trim();
if (trimmed) lines.push(trimmed);
}
}
const seenUrls = new Set();
const allUrls = [];
for (const line of lines) {
try {
const raw = line.startsWith('data:') ? line.slice(5).trim() : line;
if (!raw || raw === '[DONE]') continue;
const item = JSON.parse(raw);
const streamUrl = String(item?.url || '').trim();
if (!streamUrl || !streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue;
seenUrls.add(streamUrl);
const serverName = item.server || item.providerKey || 'Stream';
const quality = item.quality ? ` · ${item.quality}` : '';
const size = item.size ? ` (${item.size})` : '';
const label = `${serverName}${quality}${size}`;
const headers = {
'User-Agent': ua,
...(item.headers && typeof item.headers === 'object' ? item.headers : {}),
};
if (item.referer) {
headers.Referer = item.referer;
}
allUrls.push({
url: streamUrl,
label,
headers,
});
} catch { }
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
}

View File

@@ -0,0 +1,115 @@
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const API_BASE = 'https://streamdata.vaplayer.ru/api.php';
const EMBED_ORIGIN = 'https://nextgencloudfabric.com';
export const VERIFY_HEADERS = {
'User-Agent': USER_AGENT,
'Referer': `${EMBED_ORIGIN}/`,
'Origin': EMBED_ORIGIN
};
function extractStreamUrls(obj, urls = new Set()) {
if (!obj) return urls;
if (typeof obj === 'string') {
if (obj.startsWith('http') && (obj.includes('.m3u8') || obj.includes('.mp4') || obj.includes('.mpd'))) {
urls.add(obj);
}
} else if (Array.isArray(obj)) {
for (const item of obj) extractStreamUrls(item, urls);
} else if (typeof obj === 'object') {
for (const val of Object.values(obj)) {
extractStreamUrls(val, urls);
}
}
return urls;
}
export async function getStream({ id, s, e }) {
try {
const isTv = s != null && e != null;
const type = isTv ? 'tv' : 'movie';
const url = `${API_BASE}?tmdb=${id}&type=${type}${isTv ? `&season=${s}&episode=${e}` : ''}`;
const data = await fetchJson(url, {
headers: {
'Accept': '*/*',
'Origin': EMBED_ORIGIN,
'Referer': `${EMBED_ORIGIN}/`,
'User-Agent': USER_AGENT
},
signal: AbortSignal.timeout(10000)
});
const streamUrls = Array.from(extractStreamUrls(data));
if (!streamUrls.length) return null;
let subs = [];
if (data.subtitles && Array.isArray(data.subtitles)) {
subs = data.subtitles;
} else if (data.captions && Array.isArray(data.captions)) {
subs = data.captions;
}
const subtitles = subs.map(sub => ({
url: sub.url || sub.file || sub.src,
lang: sub.display || sub.label || sub.language || sub.lang || 'Unknown'
})).filter(s => s.url);
const allUrls = streamUrls.map(streamUrl => {
let type = 'mp4';
if (streamUrl.includes('.mpd')) type = 'dash';
else if (streamUrl.includes('.m3u8')) type = 'hls';
return {
url: streamUrl,
server: 'VidAPI',
quality: 'Auto',
type,
headers: VERIFY_HEADERS,
subtitles: subtitles.length > 0 ? subtitles : undefined,
skipProxy: false
};
});
return allUrls.length ? { allUrls } : null;
} catch {
return null;
}
}
export async function getSources() {
return ['VidAPI'];
}
export async function proxyStream({ url, res, proxyUtils: { fetchUpstream, rewriteM3u8 } }) {
try {
const upstream = await fetchUpstream(url, 0, VERIFY_HEADERS);
if (!upstream) return res.end();
const ct = (upstream.headers?.['content-type'] || '').toLowerCase();
const isM3u8 = ct.includes('mpegurl') || ct.includes('m3u8') || /\.m3u8?(\?|$)/i.test(url);
if (isM3u8) {
const chunks = [];
for await (const c of upstream) chunks.push(c);
let body = Buffer.concat(chunks).toString('utf8');
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
res.setHeader('Access-Control-Allow-Origin', '*');
return res.end(rewriteM3u8(body, url, '&va=1&tt=1'));
}
res.setHeader('Content-Type', ct || 'application/octet-stream');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cache-Control', 'public, max-age=3600');
upstream.pipe(res);
} catch {
if (!res.headersSent) {
res.statusCode = 502;
res.end();
}
}
}

View File

@@ -0,0 +1,123 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://vidcore.io';
const API = 'https://enc-dec.app/api';
export async function getStream({ id, s, e }) {
const isTv = Boolean(s && e);
const targetUrl = isTv
? `${BASE_URL}/tv/${id}/${s}/${e}/`
: `${BASE_URL}/movie/${id}`;
const ua = getUA();
const baseHeaders = {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
'X-Requested-With': 'XMLHttpRequest',
};
try {
const pageRes = await fetch(targetUrl, {
headers: { 'User-Agent': ua },
signal: AbortSignal.timeout(10000),
});
if (!pageRes.ok) return null;
const html = await pageRes.text();
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
if (!match || !match[1]) return null;
const enToken = match[1];
const s1Res = await fetch(`${API}/enc-vidcore?text=${encodeURIComponent(enToken)}&stage=1`, {
signal: AbortSignal.timeout(8000),
});
const s1Data = await s1Res.json();
if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
const token1 = s1Data.result.token;
const stage1Url = s1Data.result.stage1;
const post1Res = await fetch(stage1Url, {
method: 'POST',
headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
signal: AbortSignal.timeout(10000),
});
if (!post1Res.ok) return null;
const stage1Text = await post1Res.text();
const s2Res = await fetch(`${API}/enc-vidcore?text=${encodeURIComponent(stage1Text)}&stage=2`, {
signal: AbortSignal.timeout(8000),
});
const s2Data = await s2Res.json();
if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
const serversRes = await fetch(serversUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(10000),
});
if (!serversRes.ok) return null;
const serversEncrypted = await serversRes.text();
const decServersRes = await fetch(`${API}/dec-vidcore`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: serversEncrypted }),
signal: AbortSignal.timeout(8000),
});
const decServersData = await decServersRes.json();
if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
const allUrls = [];
for (const server of decServersData.result) {
if (!server?.data) continue;
try {
const streamUrl = `${streamBaseUrl}/${server.data}`;
const streamEncRes = await fetch(streamUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(8000),
});
if (!streamEncRes.ok) continue;
const streamEncText = await streamEncRes.text();
const decStreamRes = await fetch(`${API}/dec-vidcore`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: streamEncText }),
signal: AbortSignal.timeout(8000),
});
const decStreamData = await decStreamRes.json();
if (decStreamData?.status !== 200 || !decStreamData.result) continue;
const streamResult = decStreamData.result;
const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
allUrls.push({
url: resultUrl,
label: server.name || server.title || 'VidCore',
headers: {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
},
});
}
} catch { }
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
} catch {
return null;
}
}

View File

@@ -0,0 +1,123 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://vidfast.vc';
const API = 'https://enc-dec.app/api';
export async function getStream({ id, s, e }) {
const isTv = Boolean(s && e);
const targetUrl = isTv
? `${BASE_URL}/tv/${id}/${s}/${e}/`
: `${BASE_URL}/movie/${id}`;
const ua = getUA();
const baseHeaders = {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
'X-Requested-With': 'XMLHttpRequest',
};
try {
const pageRes = await fetch(targetUrl, {
headers: { 'User-Agent': ua },
signal: AbortSignal.timeout(10000),
});
if (!pageRes.ok) return null;
const html = await pageRes.text();
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
if (!match || !match[1]) return null;
const enToken = match[1];
const s1Res = await fetch(`${API}/enc-vidfast?text=${encodeURIComponent(enToken)}&stage=1`, {
signal: AbortSignal.timeout(8000),
});
const s1Data = await s1Res.json();
if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
const token1 = s1Data.result.token;
const stage1Url = s1Data.result.stage1;
const post1Res = await fetch(stage1Url, {
method: 'POST',
headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
signal: AbortSignal.timeout(10000),
});
if (!post1Res.ok) return null;
const stage1Text = await post1Res.text();
const s2Res = await fetch(`${API}/enc-vidfast?text=${encodeURIComponent(stage1Text)}&stage=2`, {
signal: AbortSignal.timeout(8000),
});
const s2Data = await s2Res.json();
if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
const serversRes = await fetch(serversUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(10000),
});
if (!serversRes.ok) return null;
const serversEncrypted = await serversRes.text();
const decServersRes = await fetch(`${API}/dec-vidfast`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: serversEncrypted }),
signal: AbortSignal.timeout(8000),
});
const decServersData = await decServersRes.json();
if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
const allUrls = [];
for (const server of decServersData.result) {
if (!server?.data) continue;
try {
const streamUrl = `${streamBaseUrl}/${server.data}`;
const streamEncRes = await fetch(streamUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(8000),
});
if (!streamEncRes.ok) continue;
const streamEncText = await streamEncRes.text();
const decStreamRes = await fetch(`${API}/dec-vidfast`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: streamEncText }),
signal: AbortSignal.timeout(8000),
});
const decStreamData = await decStreamRes.json();
if (decStreamData?.status !== 200 || !decStreamData.result) continue;
const streamResult = decStreamData.result;
const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
allUrls.push({
url: resultUrl,
label: server.name || server.title || 'VidFast',
headers: {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
},
});
}
} catch { }
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
} catch {
return null;
}
}

View File

@@ -0,0 +1,147 @@
import { USER_AGENT } from '../utils/helpers.js';
const REDIS_URL = 'https://vidnest-redis-fell-prism-rest.cloud.layerbase.dev/';
const REDIS_AUTH = 'Bearer ve8z9XSKatu74M7FjLU8eQ29';
const ORIGIN = 'https://vidgod.space';
const SERVERS = [
{
name: 'Pulsar',
key: 'pulsar',
base: 'https://vidnest-extractor.vividdubbing.workers.dev/api',
serverParam: 'prime'
},
{
name: 'Orion',
key: 'orion',
base: 'https://404-vidnest.lofiserver.workers.dev/api',
serverParam: 'gama'
},
{
name: 'Stellar',
key: 'stellar',
base: 'https://404-vidnest.lofiserver.workers.dev/api',
serverParam: 'sigma'
}
];
function buildCacheKey(serverKey, type, id, s, e) {
const parts = [serverKey, type, id];
if (type === 'tv') {
parts.push(s || '1', e || '1');
}
return parts.join(':');
}
async function getFromCache(key) {
try {
const res = await fetch(REDIS_URL, {
method: 'POST',
headers: {
'Authorization': REDIS_AUTH,
'Content-Type': 'application/json',
'Accept': '*/*',
'Origin': ORIGIN,
'Referer': `${ORIGIN}/`,
'User-Agent': USER_AGENT
},
body: JSON.stringify(['GET', key]),
signal: AbortSignal.timeout(6000)
});
if (!res.ok) return null;
const data = await res.json();
if (!data || !data.result) return null;
const parsed = typeof data.result === 'string' ? JSON.parse(data.result) : data.result;
return parsed?.streams?.length ? parsed : null;
} catch {
return null;
}
}
async function fetchFromWorker(srv, type, id, s, e) {
try {
const endpoint = type === 'movie'
? `${srv.base}/movie/${id}?server=${srv.serverParam}`
: `${srv.base}/tv/${id}/${s || '1'}/${e || '1'}?server=${srv.serverParam}`;
const res = await fetch(endpoint, {
headers: {
'Accept': 'application/json',
'Origin': ORIGIN,
'Referer': `${ORIGIN}/`,
'User-Agent': USER_AGENT
},
signal: AbortSignal.timeout(8000)
});
if (!res.ok) return null;
const data = await res.json();
return data?.streams?.length ? data : null;
} catch {
return null;
}
}
async function resolveServer(srv, type, id, s, e) {
const cacheKey = buildCacheKey(srv.key, type, id, s, e);
const cached = await getFromCache(cacheKey);
if (cached) return { server: srv.name, data: cached };
const live = await fetchFromWorker(srv, type, id, s, e);
if (live) return { server: srv.name, data: live };
return null;
}
export async function getStream(args) {
const { id, s, e, server } = args;
const isTv = s != null && e != null;
const type = isTv ? 'tv' : 'movie';
let targets = SERVERS;
if (server && server !== 'all') {
const clean = server.toLowerCase().trim();
targets = SERVERS.filter(srv => srv.name.toLowerCase() === clean || srv.key.toLowerCase() === clean);
if (!targets.length) targets = SERVERS;
}
const settled = await Promise.allSettled(
targets.map(srv => resolveServer(srv, type, id, s, e))
);
const allUrls = [];
for (const r of settled) {
if (r.status !== 'fulfilled' || !r.value) continue;
const { server: serverName, data } = r.value;
for (const item of data.streams || []) {
if (!item || !item.url) continue;
const typeStr = String(item.type || '').toLowerCase();
const urlStr = String(item.url || '').toLowerCase();
const isM3U8 =
typeStr === 'hls' ||
typeStr === 'application/vnd.apple.mpegurl' ||
typeStr === 'cloudflare' ||
typeStr === 'tiktok' ||
urlStr.includes('.m3u8') ||
urlStr.includes('.txt');
allUrls.push({
url: item.url,
server: serverName,
quality: item.quality || 'Auto',
type: isM3U8 ? 'hls' : 'mp4',
headers: item.headers || {
'User-Agent': USER_AGENT,
'Referer': `${ORIGIN}/`
}
});
}
}
return allUrls.length ? { allUrls } : null;
}

View File

@@ -0,0 +1,28 @@
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const BASE = "https://vidlink.pro";
const HEADERS = { 'User-Agent': USER_AGENT, Origin: BASE, Referer: `${BASE}/` };
export async function getStream({ id, s, e }) {
try {
const encData = await fetchJson(`https://enc-dec.app/api/enc-vidlink?text=${id}`, { signal: AbortSignal.timeout(6000) });
if (encData?.status !== 200 || !encData?.result) return null;
const data = await fetchJson(s ? `${BASE}/api/b/tv/${encData.result}/${s}/${e || 1}` : `${BASE}/api/b/movie/${encData.result}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
const stream = data?.stream;
if (!stream) return null;
if (stream.type === 'file' && stream.qualities) {
let picked = null;
for (const q of ['1080', '720', '480', '360']) if (stream.qualities[q]?.url) { picked = stream.qualities[q].url; break; }
if (!picked) picked = stream.qualities[Object.keys(stream.qualities)[0]]?.url;
if (picked) return { allUrls: [{ url: picked, type: 'mp4', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
} else if (stream.playlist) {
return { allUrls: [{ url: stream.playlist, type: 'hls', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
}
return null;
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
}

View File

@@ -0,0 +1,79 @@
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
const BASE_URL = 'https://vidnest.fun';
const API_BASE_URL = 'https://new.vidnest.fun';
const REQUEST_HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': `${BASE_URL}/`, 'Origin': BASE_URL };
const CDN_PROXY_HEADERS = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0', 'accept': '*/*', 'accept-language': 'en-US,en;q=0.5', 'origin': 'https://megaplay.buzz', 'referer': 'https://megaplay.buzz/' };
export const CDN_HEADERS = [
{ pattern: /letsgocdn\d+\.shop/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } },
{ pattern: /cdn\.mewstream\.buzz/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } }
];
const ALPHABET = 'RB0fpH8ZEyVLkv7c2i6MAJ5u3IKFDxlS1NTsnGaqmXYdUrtzjwObCgQP94hoeW+/=';
const REV_MAP = new Uint8Array(256);
for (let i = 0; i < ALPHABET.length; i++) REV_MAP[ALPHABET.charCodeAt(i)] = i;
function decrypt(payload) {
const len = payload.length;
const bytes = new Uint8Array(len);
let j = 0;
for (let i = 0; i < len; i += 4) {
const c0 = REV_MAP[payload.charCodeAt(i)] ?? 64;
const c1 = REV_MAP[payload.charCodeAt(i + 1)] ?? 64;
const c2 = REV_MAP[payload.charCodeAt(i + 2)] ?? 64;
const c3 = REV_MAP[payload.charCodeAt(i + 3)] ?? 64;
bytes[j++] = (c0 << 2) | (c1 >> 4);
if (c2 !== 64) bytes[j++] = ((c1 & 15) << 4) | (c2 >> 2);
if (c3 !== 64) bytes[j++] = ((c2 & 3) << 6) | c3;
}
return JSON.parse(new TextDecoder().decode(bytes.subarray(0, j)));
}
async function tmdbToAnilist(tmdbId, season) {
try {
const data = await fetchJson(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=tv&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
return data?.mappings?.[0]?.anilist_id || null;
} catch { return null; }
}
const SERVER_KEYS = ['hollymoviehd', 'allmovies', 'catflix', 'purstream', 'lamda', 'vidlink', 'klikxxi'];
export async function getStream({ id, s, e, sdk, audio }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
if (s) {
const k = tmdbApiKey;
const showData = k ? await fetchJson(`https://api.themoviedb.org/3/tv/${id}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }).catch(() => null) : null;
if (showData?.genres?.some(g => g.id === 16)) {
const anilistId = await tmdbToAnilist(id, s);
if (anilistId) {
try {
const json = await fetchJson(`${API_BASE_URL}/hianime/anime/${anilistId}/${e || 1}/${audio === 'dub' ? 'dub' : 'sub'}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(15000) });
const data = json.encrypted ? decrypt(json.data) : json.data;
const file = data?.sources?.[0]?.file;
if (file) return { url: `https://megacloud.animanga.fun/proxy?url=${encodeURIComponent(file)}&headers=${encodeURIComponent(JSON.stringify(CDN_PROXY_HEADERS))}`, headers: REQUEST_HEADERS };
} catch { }
}
}
}
if (audio === 'dub') return null;
const segment = s ? `tv/${id}/${s}/${e || 1}` : `movie/${id}`;
const settled = await Promise.allSettled(SERVER_KEYS.map(async server => {
const json = await fetchJson(`${API_BASE_URL}/${server}/${segment}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(10000) });
const data = json.encrypted ? decrypt(json.data) : json.data;
const file = data?.sources?.[0]?.file ?? data?.streams?.[0]?.url ?? data?.url?.[0]?.link ?? data?.data?.stream?.playlist;
if (file) return { url: file, server, headers: REQUEST_HEADERS };
throw new Error();
}));
const allUrls = [];
for (const r of settled) if (r.status === 'fulfilled') allUrls.push(r.value);
return allUrls.length ? { allUrls } : null;
}
export async function getSources(args) {
const res = await getStream(args);
return res?.allUrls ? res.allUrls.map(u => u.server) : [];
}
export const VERIFY_HEADERS = { ...REQUEST_HEADERS };

View File

@@ -0,0 +1,145 @@
import { webcrypto } from 'node:crypto';
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const GCM_HEX_KEY = '7f3e9c2a8b5d1f4e6a9c3b7d2e5f8a1c4b6d9e2f5a8c1b4d7e9f2a5c8b1d4e7f';
const BASE_URL = 'https://vidrock.ru/';
const PROXY_PREFIX = 'https://proxy.vidrock.store/';
let cryptoKey = null;
function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function base64UrlToBytes(value) {
let base64 = value.replace(/-/g, '+').replace(/_/g, '/');
const padding = base64.length % 4;
if (padding === 2) {
base64 += '==';
} else if (padding === 3) {
base64 += '=';
} else if (padding === 1) {
throw new Error('Invalid base64url');
}
return new Uint8Array(Buffer.from(base64, 'base64'));
}
async function getKey() {
if (cryptoKey) return cryptoKey;
const keyBytes = hexToBytes(GCM_HEX_KEY);
cryptoKey = await webcrypto.subtle.importKey(
'raw',
keyBytes,
'AES-GCM',
false,
['decrypt']
);
return cryptoKey;
}
async function decryptStreamUrl(value) {
try {
const data = base64UrlToBytes(value);
if (data.length < 28) {
throw new Error('Ciphertext too short');
}
const iv = data.slice(0, 12);
const ciphertext = data.slice(12);
const key = await getKey();
const decrypted = await webcrypto.subtle.decrypt(
{
name: 'AES-GCM',
iv
},
key,
ciphertext
);
return new TextDecoder().decode(decrypted);
} catch (error) {
return null;
}
}
export async function getStream({ id, s, e }) {
try {
const path = s
? `tv/${id}/${s}/${e || 1}`
: `movie/${id}`;
const apiData = await fetchJson(`${BASE_URL}api/${path}`, {
headers: {
'User-Agent': USER_AGENT,
Referer: BASE_URL,
Origin: BASE_URL
}
});
if (!apiData || typeof apiData !== 'object') {
return null;
}
const streams = [];
for (const [provider, info] of Object.entries(apiData)) {
if (!info?.url) {
continue;
}
const decryptedUrl = await decryptStreamUrl(info.url);
if (!decryptedUrl) {
continue;
}
let url = decryptedUrl;
if (url.startsWith(PROXY_PREFIX)) {
url = decodeURIComponent(
url.slice(PROXY_PREFIX.length).replace(/^\/+/, '')
);
}
const isXpass = url.includes('xpass.top');
streams.push({
name: provider,
url,
file: url,
stream: url,
src: url,
quality: info.type || 'auto',
type: info.type || 'hls',
headers: {
'User-Agent': USER_AGENT,
Referer: isXpass
? 'https://play.xpass.top/'
: BASE_URL,
Origin: isXpass
? 'https://play.xpass.top'
: 'https://vidrock.ru'
}
});
}
return streams.length ? { streams } : null;
} catch (error) {
return null;
}
}

View File

@@ -0,0 +1,123 @@
import { getUA } from '../utils/helpers.js';
const BASE_URL = 'https://vidup.to';
const API = 'https://enc-dec.app/api';
export async function getStream({ id, s, e }) {
const isTv = Boolean(s && e);
const targetUrl = isTv
? `${BASE_URL}/tv/${id}/${s}/${e}/`
: `${BASE_URL}/movie/${id}`;
const ua = getUA();
const baseHeaders = {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
'X-Requested-With': 'XMLHttpRequest',
};
try {
const pageRes = await fetch(targetUrl, {
headers: { 'User-Agent': ua },
signal: AbortSignal.timeout(10000),
});
if (!pageRes.ok) return null;
const html = await pageRes.text();
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
if (!match || !match[1]) return null;
const enToken = match[1];
const s1Res = await fetch(`${API}/enc-vidup?text=${encodeURIComponent(enToken)}&stage=1`, {
signal: AbortSignal.timeout(8000),
});
const s1Data = await s1Res.json();
if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
const token1 = s1Data.result.token;
const stage1Url = s1Data.result.stage1;
const post1Res = await fetch(stage1Url, {
method: 'POST',
headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
signal: AbortSignal.timeout(10000),
});
if (!post1Res.ok) return null;
const stage1Text = await post1Res.text();
const s2Res = await fetch(`${API}/enc-vidup?text=${encodeURIComponent(stage1Text)}&stage=2`, {
signal: AbortSignal.timeout(8000),
});
const s2Data = await s2Res.json();
if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
const serversRes = await fetch(serversUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(10000),
});
if (!serversRes.ok) return null;
const serversEncrypted = await serversRes.text();
const decServersRes = await fetch(`${API}/dec-vidup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: serversEncrypted }),
signal: AbortSignal.timeout(8000),
});
const decServersData = await decServersRes.json();
if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
const allUrls = [];
for (const server of decServersData.result) {
if (!server?.data) continue;
try {
const streamUrl = `${streamBaseUrl}/${server.data}`;
const streamEncRes = await fetch(streamUrl, {
method: 'POST',
headers: currentHeaders,
signal: AbortSignal.timeout(8000),
});
if (!streamEncRes.ok) continue;
const streamEncText = await streamEncRes.text();
const decStreamRes = await fetch(`${API}/dec-vidup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: streamEncText }),
signal: AbortSignal.timeout(8000),
});
const decStreamData = await decStreamRes.json();
if (decStreamData?.status !== 200 || !decStreamData.result) continue;
const streamResult = decStreamData.result;
const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
allUrls.push({
url: resultUrl,
label: server.name || server.title || 'VidUp',
headers: {
'User-Agent': ua,
'Referer': `${BASE_URL}/`,
},
});
}
} catch { }
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
} catch {
return null;
}
}

View File

@@ -0,0 +1,188 @@
import { USER_AGENT } from '../utils/helpers.js';
const BASE_URL = 'https://vidvault.to';
const API_BASE = `${BASE_URL}/api`;
const DL_PROXY_BASE = 'https://dl.gemlelispe.workers.dev';
const SUB_PROXY_BASE = 'https://sub.k5s7sjozpn.workers.dev';
const HEADERS = {
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL,
'User-Agent': USER_AGENT,
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
};
function formatBytes(bytes, decimals = 2) {
if (!bytes || bytes <= 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
return `${(bytes / Math.pow(k, i)).toFixed(decimals)} ${sizes[i]}`;
}
async function getToken() {
const res = await fetch(`${API_BASE}/get-token`, {
method: 'GET',
headers: HEADERS,
signal: AbortSignal.timeout(10000),
});
if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
const data = await res.json();
return data?.t;
}
function getCaptionType(url) {
return String(url).toLowerCase().includes('.vtt') ? 'vtt' : 'srt';
}
export async function getStream(args) {
const { id, s, e } = args;
const isTv = s != null && e != null;
const type = isTv ? 'tv' : 'movie';
try {
const token = await getToken();
if (!token) return null;
const payload = {
type,
tmdbId: String(id),
};
if (isTv) {
payload.season = Number(s);
payload.episode = Number(e);
}
const res = await fetch(`${API_BASE}/download-proxy`, {
method: 'POST',
headers: {
...HEADERS,
'Content-Type': 'application/json',
'x-request-token': token,
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(15000),
});
if (!res.ok) return null;
const data = await res.json();
const allUrls = [];
const subtitles = [];
const titleTag = encodeURIComponent(isTv ? `Show_${id}_S${s}E${e}` : `Movie_${id}`);
const mp4 = data?.mp4Data;
const downloadData = mp4?.downloadInfo?.data || mp4?.data?.data || mp4?.data || mp4;
if (downloadData) {
const streams = downloadData.streams || downloadData.downloads || [];
for (const item of streams) {
if (!item?.url) continue;
const res = item.resolutions || item.resolution || 'Auto';
const sizeStr = item.size ? ` (${typeof item.size === 'number' ? formatBytes(item.size) : item.size})` : '';
const proxiedUrl = `${DL_PROXY_BASE}/${encodeURIComponent(item.url)}?n=${titleTag}`;
allUrls.push({
url: proxiedUrl,
server: `VidVault - MP4 ${res}p${sizeStr}`.trim(),
quality: typeof res === 'number' ? `${res}p` : res,
type: 'mp4',
headers: HEADERS,
});
}
const captions = downloadData.captions || [];
for (const cap of captions) {
if (!cap?.url) continue;
const subUrl = `${SUB_PROXY_BASE}/?url=${encodeURIComponent(cap.url)}&title=${titleTag}`;
subtitles.push({
label: cap.lanName || cap.lan || 'Unknown',
type: getCaptionType(cap.url),
language: cap.lan || '',
url: subUrl,
});
}
}
const mkvFiles = data?.mkvData?.files || [];
for (const file of mkvFiles) {
if (!file?.url) continue;
const sizeStr = file.size ? ` (${typeof file.size === 'number' ? formatBytes(file.size) : file.size})` : '';
allUrls.push({
url: file.url,
server: `VidVault - MKV 480p${sizeStr}`.trim(),
quality: '480p',
type: 'mp4',
headers: HEADERS,
});
}
const mkvV2 = data?.mkvV2Data;
const mkvV2List = Array.isArray(mkvV2)
? mkvV2
: Array.isArray(mkvV2?.files)
? mkvV2.files
: (mkvV2?.url ? [mkvV2] : []);
for (const file of mkvV2List) {
if (!file?.url) continue;
const quality = file.quality ? String(file.quality).replace(/p$/i, '') : 'Auto';
const sizeStr = file.size ? ` (${typeof file.size === 'number' ? formatBytes(file.size) : file.size})` : '';
const meta = [file.country, file.language].filter(Boolean).join(' • ');
const metaStr = meta ? ` [${meta}]` : '';
allUrls.push({
url: file.url,
server: `VidVault - MKV v2 ${quality}p${sizeStr}${metaStr}`.trim(),
quality: `${quality}p`,
type: 'mp4',
headers: HEADERS,
});
}
const mkvV3 = data?.mkvV3Data;
if (mkvV3?.downloads && Array.isArray(mkvV3.downloads)) {
const country = mkvV3.country || '';
const language = mkvV3.language || '';
for (const down of mkvV3.downloads) {
if (Array.isArray(down?.qualities)) {
for (const qObj of down.qualities) {
for (const ep of qObj?.episodes || []) {
if (!ep?.url) continue;
const quality = qObj.quality ? String(qObj.quality).replace(/p$/i, '') : 'Auto';
const sizeStr = ep.size ? ` (${typeof ep.size === 'number' ? formatBytes(ep.size) : ep.size})` : '';
const meta = [ep.country || country, ep.language || language].filter(Boolean).join(' • ');
const metaStr = meta ? ` [${meta}]` : '';
allUrls.push({
url: ep.url,
server: `VidVault - MKV v3 ${quality}p${sizeStr}${metaStr}`.trim(),
quality: `${quality}p`,
type: 'mp4',
headers: HEADERS,
});
}
}
} else if (down?.url) {
const quality = down.quality ? String(down.quality).replace(/p$/i, '') : 'Auto';
const sizeStr = down.size ? ` (${typeof down.size === 'number' ? formatBytes(down.size) : down.size})` : '';
allUrls.push({
url: down.url,
server: `VidVault - MKV v3 ${quality}p${sizeStr}`.trim(),
quality: `${quality}p`,
type: 'mp4',
headers: HEADERS,
});
}
}
}
return allUrls.length ? { allUrls, subtitles: subtitles.length ? subtitles : undefined } : null;
} catch (err) {
return null;
}
}

View File

@@ -0,0 +1,184 @@
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
const BASE_URL = 'https://core.vidzee.wtf';
const PLAYER_URL = 'https://player.vidzee.wtf';
const SERVICES = ['dcloud', 'ipcloud', 'tik', 'v6:Hindi', '3AHindi'];
const WASM_B64 = 'AGFzbQEAAAABPgtgAX8Bf2ACf38Bf2ABfwBgAn9/AGADf39/AGAAAGAEf39/fwBgBH9/f38Bf2ADf39/AX9gA39/fgBgAAF/Ag0BA2VudgVhYm9ydAAGAygnAgEAAAQBAAEDBAADBQAAAgMHAAEAAAQIAwEJAgIBBQAAAgEABQoBBQMBAAEGJgd/AUEAC38BQQALfwFBAAt/AUEAC38BQQALfwFBAAt/AEGA2wALB0gHB2RlY3J5cHQAJwVfX25ldwAGBV9fcGluACQHX191bnBpbgAiCV9fY29sbGVjdAAlC19fcnR0aV9iYXNlAwYGbWVtb3J5AgAIAR8MAqkBCogkJyUAIABFBEAPCyAAQRRrIgAQByMCRgRAIAAQECAAIwUjAkUQCgsLCgAgACABai0AAAsKACAAKAIEQXxxCw0AIABBFGsoAhBBAXYLDAAgACABaiACOgAAC1MBAX8gAEHs////A0sEQEHwDEGwDUH9AEEeEAAACyMARQRAEA0LIwAgAEEQahAaIgIgATYCDCACIAA2AhAgAiMBIwIQCiACEAsjA2okAyACQRRqCwoAIAAoAgRBA3ELKQAgACAAIAFBCHZqLQAAQQV0aiABQf8BcUEDdmotAAAgAUEHcXZBAXELwwEBBH8gASgCAEF8cSIDQYACSQR/IANBBHYFQR9B/P///wMgAyADQfz///8DTxsiA2drIgRBB2shAiADIARBBGt2QRBzCyEEIAEoAgghBSABKAIEIgMEQCADIAU2AggLIAUEQCAFIAM2AgQLIAEgACACQQR0IARqQQJ0aiIBKAJgRgRAIAEgBTYCYCAFRQRAIAAgAkECdGoiASgCBEF+IAR3cSEDIAEgAzYCBCADRQRAIAAgACgCAEF+IAJ3cTYCAAsLCwspAQF/IAEoAgghAyAAIAEgAnI2AgQgACADNgIIIAMgABARIAEgADYCCAsNACAAKAIAQXxxQQRqC7wCAQV/IAEoAgAhAyABQQRqIAEoAgBBfHFqIgQoAgAiAkEBcQRAIAAgBBAJIAEgA0EEaiACQXxxaiIDNgIAIAFBBGogASgCAEF8cWoiBCgCACECCyADQQJxBEAgAUEEaygCACIBKAIAIQYgACABEAkgASAGQQRqIANBfHFqIgM2AgALIAQgAkECcjYCACAEQQRrIAE2AgAgACADQXxxIgJBgAJJBH8gAkEEdgVBH0H8////AyACIAJB/P///wNPGyICZ2siA0EHayEFIAIgA0EEa3ZBEHMLIgIgBUEEdGpBAnRqKAJgIQMgAUEANgIEIAEgAzYCCCADBEAgAyABNgIECyAAIAVBBHQgAmpBAnRqIAE2AmAgACAAKAIAQQEgBXRyNgIAIAAgBUECdGoiACAAKAIEQQEgAnRyNgIEC5cBAQJ/PwAiAEEATAR/QQEgAGtAAEEASAVBAAsEQAALQbDbAEEANgIAQdDnAEEANgIAA0AgAUEXSQRAIAFBAnRBsNsAakEANgIEQQAhAANAIABBEEkEQCABQQR0IABqQQJ0QbDbAGkBADYCYCAAQQFqIQAMAQsLIAFBAWohAQwBCwtBsNsAQdTnAD8ArEIQhhAbQbDbACQACyYBAX8gAEEEayEBIABBD3FBASAAGwR/QQEFIAEoAgBBAXELGiABCxIAIAAgADYCBCAAIAA2AgggAAsnAQF/IAAQAyIBRQRAIAAoAggaDwsgASAAKAIIIgA2AgggACABEBELEgAgACABIAAoAgRBA3FyNgIEC0kBAX8gACABQQF0aiEBA0AgAyIAQQFrIQMgAARAIAEvAQAiACACLwEAIgRHBEAgACAEaw8LIAFBAmohASACQQJqIQIMAQsLQQALbgECf0EMQQYQBiIBRQRAQQxBAxAGIQELIAFBADYCACABQQA2AgQgAUEANgIIIABB/P///wNLBEBB4NcAQZDYAEETQTkQAAALIABBARAGIgJBACAA/AsAIAEgAjYCACABIAI2AgQgASAANgIIIAELjgEBAn8gAUGAAkkEfyABQQR2BUEfIAEQFSIBZ2siA0EHayECIAEgA0EEa3ZBEHMLIgEgACACQQJ0aigCBEF/IAF0cSIBBH8gACABaCACQQR0akECdGooAmAFIAAoAgBBfyACQQFqdHEiAQR/IAAgACABaCIAQQJ0aigCBGggAEEEdGpBAnRqKAJgBUEACwsLHQAgAEEBQRsgAGdrdGpBAWsgACAAQf7///8BSRsLLwAgAEH8////A0sEQEHwDEHwDUHNA0EdEAAAC0EMIABBE2pBcHFBBGsgAEEMTRsLZwECfyABKAIAIgNBfHEgAmsiBEEQTwRAIAEgAiADQQJxcjYCACABQQRqIAJqIgEgBEEEa0EBcjYCACAAIAEQDAUgASADQX5xNgIAIAFBBGogASgCAEF8cWoiACAAKAIAQX1xNgIACwswACAAIAIQGiICQQRqIAFBBGogASgCAEF8cfwKAAAgAUGk2wBPBEAgACABEBkLIAILFQAgASABKAIAQQFyNgIAIAAgARAMC48BAQJ/IAAgARAWIgIQFCIBRQRAQQQgACgCoAw/ACIBQRB0QQRrR3QgAhAVIAIgAkGAAk8bakH//wNqQYCAfHFBEHYhAyABIAMgASADShtAAEEASARAIANAAEEASARAAAsLIAAgAUEQdD8ArEIQhhAbIAAgAhAUIQELIAEoAgAaIAAgARAJIAAgASACEBcgAQuGAQEDfyABQRNqQXBxQQRrIQEgACgCoAwiAwRAIAFBEGsiBSADRgRAIAMoAgAhBCAFIQELCyACp0FwcSABayIDQRRJBEAPCyABIARBAnEgA0EIayIDQQFycjYCACABQQA2AgQgAUEANgIIIAFBBGogA2oiA0ECNgIAIAAgAzYCoAwgACABEAwLhQEBA38CQAJAAkACQAJAAkACQAJAAkAgAEEIaygCAA4IAAECAwQFBgcICw8LDwsPCyAAEB0PCyAAKAIAEAEPCyAAKAIEIgEgACgCDEECdGohAgNAIAEgAkkEQCABKAIAIgMEQCADEAELIAFBBGohAQwBCwsgACgCABABDwsgABAdDwsPCwALCQAgACgCABABC0MAIAEgACgCDE8EQEHw1QBBsNYAQfIAQSoQAAALIAAoAgQgAUECdGooAgAiAEUEQEHg1gBBsNYAQfYAQSgQAAALIAALGQBBoA4QDyQBQYDaABAPJARB4NoAEA8kBQv9AQEGfyAAIgFBCHYiAkHMzgBqLQAAIAJBlDJqLQAAQdYAbEGUMmogAEH/AXEiA0EDbmotAAAgA0EDcEECdEGAxwBqKAIAbEELdkEGcGpBAnRBjbrowserigCACICQf8BcSEAIAJBCHUhAgJAIABBAkkNACACQf8BcSEAIAJBCHYhAgNAIAAEQCAAQQF2IgYgAmpBAXRBzNIAaiIELQAAIgUgA0YEfyAELQABQQJ0QYzHAGooAgAiAkH/AXEhACACQQh1IQIgAEECSQ0DIAFBAWoPBSADIAVJBH8gBgUgAiAGaiECIAAgBmsLCyEADAELCyABDwsgASACQQAgAGtxag==';
let wasmInstanceCache = null;
async function getWasmInstance() {
if (wasmInstanceCache) return wasmInstanceCache;
try {
const wasmBuffer = Buffer.from(WASM_B64, 'base64');
const wasmModule = await WebAssembly.compile(wasmBuffer);
let memory;
let exports;
const env = {
abort() {
throw new Error('WASM abort');
}
};
const res = await WebAssembly.instantiate(wasmModule, { env });
exports = res.exports;
memory = exports.memory;
const refMap = new Map();
function pin(ptr) {
if (ptr) {
const count = refMap.get(ptr);
if (count) refMap.set(ptr, count + 1);
else refMap.set(exports.__pin(ptr), 1);
}
return ptr;
}
function unpin(ptr) {
if (ptr) {
const count = refMap.get(ptr);
if (count === 1) {
exports.__unpin(ptr);
refMap.delete(ptr);
} else if (count) {
refMap.set(ptr, count - 1);
}
}
}
wasmInstanceCache = {
decrypt(encryptedBytes, keyStr) {
let dataPtr = 0;
let keyPtr = 0;
try {
const len = encryptedBytes.length;
const pinPtr = exports.__pin(exports.__new(len, 1)) >>> 0;
dataPtr = exports.__new(12, 6) >>> 0;
const view = new DataView(memory.buffer);
view.setUint32(dataPtr, pinPtr, true);
view.setUint32(dataPtr + 4, pinPtr, true);
view.setUint32(dataPtr + 8, len, true);
new Uint8Array(memory.buffer, pinPtr, len).set(encryptedBytes);
exports.__unpin(pinPtr);
pin(dataPtr);
const kLen = keyStr.length;
keyPtr = exports.__new(kLen << 1, 2) >>> 0;
const u16View = new Uint16Array(memory.buffer);
for (let i = 0; i < kLen; i++) {
u16View[(keyPtr >>> 1) + i] = keyStr.charCodeAt(i);
}
const resPtr = exports.decrypt(dataPtr, keyPtr) >>> 0;
if (!resPtr) return new Uint8Array(0);
const resView = new DataView(memory.buffer);
const outPtr = resView.getUint32(resPtr + 4, true);
const outLen = resView.getUint32(resPtr + 8, true);
return new Uint8Array(memory.buffer, outPtr, outLen).slice();
} finally {
unpin(dataPtr);
}
}
};
return wasmInstanceCache;
} catch {
return null;
}
}
async function requestEndpoint(url, headers) {
try {
return await fetchJson(url, {
headers,
signal: AbortSignal.timeout(6000)
});
} catch {
return null;
}
}
async function resolveServiceStream(service, isMovie, id, s, e, headers) {
const directEndpoint = isMovie
? `${BASE_URL}/streams/movie/${id}?s=${service}`
: `${BASE_URL}/streams/tv/${id}/${s || 1}/${e || 1}?s=${service}`;
let res = await requestEndpoint(directEndpoint, headers);
if (!res || !res.url) {
const encEndpoint = `${directEndpoint}&e=1`;
const encRes = await requestEndpoint(encEndpoint, headers);
if (encRes && encRes.c) {
const instance = await getWasmInstance();
if (instance) {
const encryptedBytes = new Uint8Array(Buffer.from(encRes.c, 'base64'));
const decryptedBytes = instance.decrypt(encryptedBytes, 'player.vidzee.wtf');
if (decryptedBytes.length > 0) {
const str = new TextDecoder().decode(decryptedBytes);
try {
res = JSON.parse(str);
} catch { }
}
}
}
}
if (res && res.url) {
const format = res.url.includes('.m3u8') ? 'hls' : 'mp4';
return {
url: res.url,
server: `VidZee (${service})`,
quality: res.language || 'Auto',
type: format,
headers: {
'User-Agent': USER_AGENT,
'Referer': `${PLAYER_URL}/`,
'Origin': PLAYER_URL,
...(res.headers || {})
}
};
}
return null;
}
export async function getStream(args) {
const { id, s, e, server, clientIP } = args;
const isTv = s != null && e != null;
const isMovie = !isTv;
const headers = {
'User-Agent': USER_AGENT,
'Accept': 'application/json, text/plain, */*',
'Referer': `${PLAYER_URL}/`,
'Origin': PLAYER_URL,
...(clientIP && { 'X-Forwarded-For': clientIP })
};
let targets = SERVICES;
if (server && server !== 'all') {
const clean = server.toLowerCase().replace('vidzee (', '').replace(')', '').replace('vidzee', '').trim();
targets = SERVICES.filter(srv => srv.toLowerCase().includes(clean));
if (!targets.length) targets = SERVICES;
}
const settled = await Promise.allSettled(
targets.map(service => resolveServiceStream(service, isMovie, id, s, e, headers))
);
const allUrls = [];
for (const r of settled) {
if (r.status === 'fulfilled' && r.value) {
allUrls.push(r.value);
}
}
return allUrls.length ? { allUrls } : null;
}
export async function getSources() {
return SERVICES.map(s => `VidZee (${s})`);
}

View File

@@ -0,0 +1,112 @@
import { fetchText, USER_AGENT } from '../utils/helpers.js';
const BASE_URL = 'https://vixsrc.to';
const HEADERS = {
'User-Agent': USER_AGENT,
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL
};
function extract(regex, text) {
const m = text?.match(regex);
if (!m) return null;
let val = m[1];
val = val.replace(/\\u0026/gi, '&');
val = val.replace(/\\\//g, '/');
val = val.replace(/\\/g, '');
return val;
}
export async function getStream({ id, s, e, absoluteBase }) {
try {
const apiUrl = s && e
? `${BASE_URL}/api/tv/${id}/${s}/${e}`
: `${BASE_URL}/api/movie/${id}`;
const apiProxy = absoluteBase && absoluteBase !== '/'
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(apiUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
: apiUrl;
const apiText = await fetchText(apiProxy, { headers: HEADERS }).catch(() => null);
const apiData = JSON.parse(apiText || '{}');
if (!apiData.src) return null;
const embedUrl = apiData.src.startsWith('http') ? apiData.src : `${BASE_URL}${apiData.src}`;
const embedProxy = absoluteBase && absoluteBase !== '/'
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(embedUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
: embedUrl;
const html = await fetchText(embedProxy, { headers: HEADERS }).catch(() => null);
if (!html) return null;
const token = extract(/token["']\s*:\s*["']([^"']+)/, html);
const expires = extract(/expires["']\s*:\s*["']([^"']+)/, html);
let playlist = extract(/url["']\s*:\s*["']([^"']+)/, html);
if (!token || !expires || !playlist) return null;
if (playlist.startsWith('/')) {
playlist = `${BASE_URL}${playlist}`;
}
const lang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${lang}`;
const masterProxy = absoluteBase && absoluteBase !== '/'
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify({ ...HEADERS, Referer: embedUrl }))}`
: rawMasterUrl;
const playlistText = await fetchText(masterProxy, { headers: { ...HEADERS, 'Referer': embedUrl } }).catch(() => null);
if (!playlistText?.includes('#EXTM3U')) return null;
let finalUrl = null;
let bestHeight = 0;
const lines = playlistText.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line.startsWith('#EXT-X-STREAM-INF')) continue;
const height = parseInt(line.match(/RESOLUTION=\d+x(\d+)/)?.[1] || '0', 10);
let next = lines[i + 1]?.trim();
if (!next || next.startsWith('#')) continue;
if (height > bestHeight) {
bestHeight = height;
finalUrl = next;
}
}
if (!finalUrl) finalUrl = rawMasterUrl;
if (!finalUrl.startsWith('http')) {
const baseMatch = rawMasterUrl.match(/^(https?:\/\/[^\/]+)/);
const base = baseMatch ? baseMatch[1] : BASE_URL;
if (finalUrl.startsWith('/')) {
finalUrl = `${base}${finalUrl}`;
} else {
const pathParts = rawMasterUrl.split('/');
pathParts.pop();
finalUrl = `${pathParts.join('/')}/${finalUrl}`;
}
}
return {
allUrls: [
{
url: finalUrl,
server: 'VixSrc',
skipProxy: true,
headers: {
...HEADERS,
'Referer': `${BASE_URL}/`
}
}
]
};
} catch (err) {
return null;
}
}

View File

@@ -0,0 +1,129 @@
const BASE_URL = 'https://www.films365.org';
const HEADERS = {
Authorization: 'Bearer 79a02956be35835728a044b11e2ae793149d45fb2c89cb6d029ec01aac19bfdb',
'Content-Type': 'application/json',
'User-Agent': 'MovieDownloader/1.0',
};
async function resolveTitle(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}`, {
signal: AbortSignal.timeout(6000),
});
if (!res.ok) return null;
const data = await res.json();
return isTv ? data.name : data.title;
} catch {
return null;
}
}
export async function getStream({ id, s, e, sdk }) {
const isTv = Boolean(s && e);
const targetType = isTv ? 'tv' : 'movie';
const title = await resolveTitle(sdk, id, isTv);
const query = title || String(id);
const searchRes = await fetch(`${BASE_URL}/api/mobile/search`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ query }),
signal: AbortSignal.timeout(8000),
});
if (!searchRes.ok) return null;
const searchJson = await searchRes.json();
const resultsObj = searchJson?.results;
if (!resultsObj) return null;
const items = resultsObj.all || (targetType === 'tv' ? resultsObj.tvs : resultsObj.movies) || [];
if (!Array.isArray(items) || items.length === 0) return null;
const cleanSearchTitle = query.toLowerCase().replace(/[^a-z0-9]/g, '');
let matchedItem = null;
for (const item of items) {
const itemType = String(item?.type || '').toLowerCase();
if (itemType && itemType !== targetType) continue;
const itemTitle = String(item?.title || '');
const cleanItemTitle = itemTitle.toLowerCase().replace(/[^a-z0-9]/g, '');
if (cleanItemTitle === cleanSearchTitle || cleanItemTitle.includes(cleanSearchTitle)) {
matchedItem = item;
break;
}
}
if (!matchedItem) {
matchedItem = items.find(i => String(i?.type || '').toLowerCase() === targetType) || items[0];
}
const itemId = matchedItem?.id || matchedItem?.tmdbId;
if (!itemId) return null;
const detailsRes = await fetch(`${BASE_URL}/api/mobile/details?id=${itemId}&type=${targetType}`, {
headers: HEADERS,
signal: AbortSignal.timeout(8000),
});
if (!detailsRes.ok) return null;
const detailsJson = await detailsRes.json();
const data = detailsJson?.data;
if (!data) return null;
const spoken = Array.isArray(data.spokenLanguages)
? data.spokenLanguages.map(l => String(l).trim()).filter(Boolean)
: [];
const langSuffix = spoken.length > 0 ? ` · ${spoken.join(', ')}` : '';
const allUrls = [];
if (!isTv) {
const streamUrl = data.downloadUrl || data.videoUrl;
if (streamUrl && typeof streamUrl === 'string') {
allUrls.push({
url: streamUrl.trim(),
label: `X-Downloader${langSuffix}`,
headers: {
'User-Agent': HEADERS['User-Agent'],
},
});
}
} else {
const seasonNum = Number(s);
const episodeNum = Number(e);
const seasons = Array.isArray(data.seasons) ? data.seasons : [];
const targetSeason = seasons.find(sn => Number(sn.seasonNumber) === seasonNum);
if (targetSeason) {
const episodes = Array.isArray(targetSeason.episodes) ? targetSeason.episodes : [];
const targetEp = episodes.find(ep => Number(ep.episodeNumber) === episodeNum);
if (targetEp) {
const streamUrl = targetEp.downloadUrl || targetEp.videoUrl;
if (streamUrl && typeof streamUrl === 'string') {
allUrls.push({
url: streamUrl.trim(),
label: `X-Downloader (S${seasonNum}E${episodeNum})${langSuffix}`,
headers: {
'User-Agent': HEADERS['User-Agent'],
},
});
}
}
}
}
if (allUrls.length === 0) return null;
return {
url: allUrls[0].url,
allUrls,
headers: allUrls[0].headers,
};
}

122
node_modules/@vyla-entertainment/sdk/src/subtitles.js generated vendored Normal file
View File

@@ -0,0 +1,122 @@
import { getUA } from './utils/helpers.js';
const SUBTITLE_BASES = [
'https://sub.vdrk.site/v1',
'https://sub.vdrk.site/v2',
'https://opensubtitles-v3.strem.io'
];
async function resolveImdbId(id, isTv) {
if (typeof id === 'string' && id.startsWith('tt')) return id;
const apiKey = process.env.TMDB_API_KEY;
if (!apiKey) return `tt${id}`;
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(5000),
});
if (!res.ok) return `tt${id}`;
const data = await res.json();
return data.imdb_id || data.external_ids?.imdb_id || `tt${id}`;
} catch {
return `tt${id}`;
}
}
export async function fetchSubtitles(paths = []) {
try {
const results = await Promise.all(
paths.map(async ({ base, path }) => {
try {
const res = await fetch(`${base}${path}`, {
headers: { 'User-Agent': getUA() },
signal: AbortSignal.timeout(6000),
});
if (!res.ok) {
res.body?.cancel();
return [];
}
const data = await res.json();
if (base.includes('/v2')) {
return Array.isArray(data)
? data.map(x => ({
label: x.label,
file: x.file || x.url,
type: 'vtt',
source: 'v2'
}))
: [];
}
if (base.includes('opensubtitles-v3.strem.io')) {
if (!Array.isArray(data?.subtitles)) return [];
return data.subtitles.map(sub => {
if (!sub?.url) return null;
const isSrt = sub.url.toLowerCase().endsWith('.srt');
return {
label: sub.lang ? sub.lang.toUpperCase() : 'OpenSubtitles',
file: sub.url,
type: isSrt ? 'srt' : 'vtt',
source: 'opensubtitles'
};
}).filter(Boolean);
}
if (base.includes('fed-subs.pstream.mov')) {
if (!data?.subtitles || typeof data.subtitles !== 'object') return [];
return Object.entries(data.subtitles)
.map(([language, sub]) => {
if (!sub?.subtitle_link) return null;
const ext = sub.subtitle_link.split('.').pop()?.toLowerCase();
return {
label: sub.subtitle_name || language,
file: sub.subtitle_link,
type: ext === 'vtt' ? 'vtt' : 'srt',
source: 'febbox'
};
})
.filter(Boolean);
}
const v1 = Array.isArray(data) ? data : [];
return v1.map(x => ({
label: x.label,
file: x.file || x.url,
type: 'vtt',
source: 'v1'
}));
} catch {
return [];
}
})
);
return results.flat();
} catch {
return [];
}
}
export async function getSubPathsMovie(id) {
const imdbId = await resolveImdbId(id, false);
return [
{ base: SUBTITLE_BASES[0], path: `/movie/${id}` },
{ base: SUBTITLE_BASES[1], path: `/movie/${id}` },
{ base: SUBTITLE_BASES[2], path: `/subtitles/movie/${imdbId}.json` }
];
}
export async function getSubPathsTv(id, season, episode) {
const imdbId = await resolveImdbId(id, true);
return [
{ base: SUBTITLE_BASES[0], path: `/tv/${id}/${season}/${episode}` },
{ base: SUBTITLE_BASES[1], path: `/tv/${id}/${season}/${episode}` },
{ base: SUBTITLE_BASES[2], path: `/subtitles/series/${imdbId}:${season}:${episode}.json` }
];
}
export { SUBTITLE_BASES };

View File

@@ -0,0 +1,230 @@
import crypto from 'node:crypto';
export const UA_LIST = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
];
export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)];
export const USER_AGENT = UA_LIST[0];
let sharedDcKey = null;
export function decryptPartner(responseText, keyEnv) {
try {
const sep = responseText.indexOf(':');
if (sep === -1) return null;
const iv = Buffer.from(responseText.slice(0, sep), 'base64');
const encrypted = responseText.slice(sep + 1);
if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest();
const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv);
return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8'));
} catch {
return null;
}
}
export async function fetchJson(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export async function fetchText(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
}
const tmdbInfoCache = new Map();
const anilistCache = new Map();
const tmdbValidationCache = new Map();
function pruneHelperCache(cache) {
const now = Date.now();
for (const [k, v] of cache) if (now - v.ts >= v.ttl) cache.delete(k);
if (cache.size > 3000) {
const overflow = cache.size - 3000;
const it = cache.keys();
for (let i = 0; i < overflow; i++) cache.delete(it.next().value);
}
}
setInterval(() => {
pruneHelperCache(tmdbValidationCache);
pruneHelperCache(tmdbInfoCache);
pruneHelperCache(anilistCache);
}, 60000).unref();
function cacheGet(cache, key) {
const entry = cache.get(key);
if (!entry) return undefined;
if (Date.now() - entry.ts >= entry.ttl) { cache.delete(key); return undefined; }
return entry.val;
}
function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); }
export async function createStreamArgs(source, sdk, id, s, e, clientIP = null) {
const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
return {
id: id,
s: s,
e: e,
clientIP: clientIP,
absoluteBase: '/',
audio,
sdk,
config: source,
}
}
export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
let apiKey = tmdbApiKey;
let id = tmdbId;
let type = mediaType;
if (id === 'movie' || id === 'tv' || !id) {
if (apiKey && apiKey !== 'movie' && apiKey !== 'tv') {
type = id || 'movie';
id = apiKey;
apiKey = process.env.TMDB_API_KEY;
}
}
if (!apiKey) {
apiKey = process.env.TMDB_API_KEY;
}
const key = `${id}-${type}`;
const cached = cacheGet(tmdbValidationCache, key);
if (cached !== undefined) return cached;
if (!apiKey) return { valid: false, error: 'TMDB API key not configured' };
try {
const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) });
if (res.ok) {
const data = await res.json();
const isValid = !!(data?.id && (data?.title || data?.name));
const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' };
cacheSet(tmdbValidationCache, key, result, 300000);
return result;
}
const result = { valid: false, error: `TMDB API error: ${res.status}` };
cacheSet(tmdbValidationCache, key, result, 60000);
return result;
} catch (error) {
const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` };
cacheSet(tmdbValidationCache, key, result, 60000);
return result;
}
}
export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
let apiKey = tmdbApiKey;
let id = tmdbId;
let type = mediaType;
let s = season;
if (apiKey === 'movie' || apiKey === 'tv') {
type = apiKey;
id = tmdbId;
apiKey = process.env.TMDB_API_KEY;
} else if (id === 'movie' || id === 'tv') {
type = id;
id = apiKey;
apiKey = process.env.TMDB_API_KEY;
}
if (!apiKey) {
apiKey = process.env.TMDB_API_KEY;
}
const key = `${id}-${type}-${s || ''}`;
const cached = cacheGet(tmdbInfoCache, key);
if (cached !== undefined) return cached;
if (!apiKey) return { isAnime: false, titles: [], year: null, imdbId: null };
try {
const [mainRes, seasonRes] = await Promise.all([
fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }),
s && type === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null),
]);
let mainData = null, seasonData = null;
if (mainRes.ok) mainData = await mainRes.json(); else mainRes.body?.cancel();
if (seasonRes) { if (seasonRes.ok) seasonData = await seasonRes.json(); else seasonRes.body?.cancel(); }
const genres = mainData?.genres || [];
const isAnime = genres.some(g => g.id === 16) && ((mainData?.origin_country || []).includes('JP') || mainData?.original_language === 'ja');
const titles = [];
if (seasonData?.name) titles.push(seasonData.name);
const titleText = mainData?.title || mainData?.name || '';
const ot = mainData?.original_title || mainData?.original_name || '';
if (titleText) titles.push(titleText);
if (ot && ot !== titleText) titles.push(ot);
const dateStr = seasonData?.air_date || mainData?.release_date || mainData?.first_air_date || '';
const result = { isAnime, titles: [...new Set(titles.filter(Boolean))], year: dateStr ? parseInt(dateStr.slice(0, 4), 10) : null, imdbId: mainData?.imdb_id || mainData?.external_ids?.imdb_id || null };
cacheSet(tmdbInfoCache, key, result, 600000);
return result;
} catch { return { isAnime: false, titles: [], year: null, imdbId: null }; }
}
export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) {
const key = `${tmdbId}-${mediaType}-${season || ''}`;
const cached = cacheGet(anilistCache, key);
if (cached !== undefined) return cached;
try {
const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
if (res.ok) {
const id = (await res.json())?.mappings?.[0]?.anilist_id;
if (id) { cacheSet(anilistCache, key, id, 600000); return id; }
} else res.body?.cancel();
} catch { }
if (!titles.length) return null;
const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`;
let bestId = null, bestScore = -1;
for (const searchTitle of titles) {
try {
const res = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query, variables: { s: searchTitle } }), signal: AbortSignal.timeout(6000) });
if (!res.ok) { res.body?.cancel(); continue; }
for (const entry of (await res.json())?.data?.Page?.media || []) {
const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase());
const sl = searchTitle.toLowerCase();
let score = 0;
if (entryTitles.some(t => t === sl)) score += 5;
else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3;
else if (entryTitles.some(t => { const w = sl.split(/\s+/).filter(x => x.length > 3); return w.length > 0 && w.every(x => t.includes(x)); })) score += 2;
else continue;
if (year && entry.startDate?.year) {
const diff = Math.abs(entry.startDate.year - year);
if (diff === 0) score += 3; else if (diff === 1) score += 1; else if (diff > 2) score -= 3;
}
if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1;
if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1;
if (score > bestScore) { bestScore = score; bestId = entry.id; }
}
if (bestScore >= 8) break;
} catch { }
}
if (bestId) cacheSet(anilistCache, key, bestId, 600000);
return bestId;
}
export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
let apiKey = tmdbApiKey;
let id = imdbId;
let type = mediaType;
if (id === 'movie' || id === 'tv') {
type = id;
id = apiKey;
apiKey = process.env.TMDB_API_KEY;
}
if (!apiKey) {
apiKey = process.env.TMDB_API_KEY;
}
const key = `imdb-${id}`;
const cached = cacheGet(tmdbInfoCache, key);
if (cached !== undefined) return cached;
if (!apiKey) return null;
try {
const data = await fetchJson(`https://api.themoviedb.org/3/find/${id}?api_key=${apiKey}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) });
let result = null;
if (type === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
else if (type === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
if (result) cacheSet(tmdbInfoCache, key, result, 600000);
return result;
} catch { return null; }
}

22
package-lock.json generated Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "tv",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tv",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@vyla-entertainment/sdk": "^1.14.0"
}
},
"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"
}
}
}

25
package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "tv",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"start": "node server.js",
"cap:init": "cap add android",
"cap:sync": "cap sync android",
"dev": "cap sync android && cd android && gradlew assembleDebug",
"release": "cap sync android && cd android && gradlew assembleRelease"
},
"dependencies": {
"@capacitor/android": "^6.0.0",
"@capacitor/cli": "^6.0.0",
"@capacitor/core": "^6.0.0",
"@vyla-entertainment/sdk": "^1.14.0",
"dotenv": "^16.4.5",
"express": "^4.19.2"
},
"main": "server.js",
"keywords": [],
"author": "",
"license": "ISC"
}

348
server.js Normal file
View File

@@ -0,0 +1,348 @@
import express from 'express';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import VylaSDK from '@vyla-entertainment/sdk';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3000;
const sdk = new VylaSDK({
tmdbApiKey: process.env.TMDB_API_KEY || '3f6703f3e2a0b2b40fe4fbb3e1546244'
});
app.use(express.static(__dirname));
function getCleanClientIp(req) {
let ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || '';
if (ip.includes(',')) ip = ip.split(',')[0].trim();
if (ip.includes('::ffff:')) ip = ip.replace('::ffff:', '');
if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.') || !ip) {
return undefined;
}
return ip;
}
app.get('/api/sources', (req, res) => {
try {
const sources = sdk.getSources(true);
res.json({ sources });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
async function isStreamPlayable(streamUrl, headers = {}) {
try {
const fetchOpts = {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9',
...headers,
'Range': 'bytes=0-1024'
},
redirect: 'follow',
signal: AbortSignal.timeout(6000)
};
const res = await fetch(streamUrl, fetchOpts);
if (![200, 206, 304].includes(res.status)) return false;
const ct = (res.headers.get('content-type') || '').toLowerCase();
const isM3U8 = streamUrl.includes('.m3u8') || ct.includes('mpegurl') || ct.includes('m3u8');
if (isM3U8) {
const text = await res.text();
const lines = text.split('\n');
const baseUrl = streamUrl.substring(0, streamUrl.lastIndexOf('/') + 1);
const originBase = new URL(streamUrl).origin;
let firstTarget = null;
for (let i = 0; i < lines.length; i++) {
const t = lines[i].trim();
if (t && !t.startsWith('#')) {
firstTarget = t.startsWith('http') ? t : t.startsWith('/') ? originBase + t : baseUrl + t;
break;
}
}
if (!firstTarget) return false;
if (firstTarget.includes('.m3u8') || firstTarget.includes('m3u8')) {
return await isStreamPlayable(firstTarget, headers);
}
const chunkRes = await fetch(firstTarget, {
method: 'GET',
headers: {
'User-Agent': fetchOpts.headers['User-Agent'],
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9',
...headers,
'Range': 'bytes=0-1024'
},
redirect: 'follow',
signal: AbortSignal.timeout(6000)
});
return [200, 206, 304].includes(chunkRes.status);
}
return true;
} catch (_) {
return false;
}
}
app.get('/api/stream', async (req, res) => {
const { provider, tmdbId, season, episode } = req.query;
if (!provider || !tmdbId) {
return res.status(400).json({ error: 'Missing provider or tmdbId parameter' });
}
const clientIP = getCleanClientIp(req);
try {
const s = season && season !== 'null' && season !== 'undefined' ? season : undefined;
const e = episode && episode !== 'null' && episode !== 'undefined' ? episode : undefined;
const streamData = await sdk.getStream(
String(provider),
String(tmdbId),
s,
e,
clientIP
);
if (!streamData) return res.status(404).json({ error: 'Stream not found' });
const customHeaders = streamData.headers || {};
if (Array.isArray(streamData.allUrls) && streamData.allUrls.length > 0) {
const checks = await Promise.all(
streamData.allUrls.map(async (u) => {
const ok = u && u.url ? await isStreamPlayable(u.url, customHeaders) : false;
return ok ? u : null;
})
);
streamData.allUrls = checks.filter(Boolean);
if (streamData.allUrls.length > 0) {
if (!streamData.url || !streamData.allUrls.some(u => u.url === streamData.url)) {
streamData.url = streamData.allUrls[0].url;
}
} else {
streamData.url = null;
}
} else if (streamData.url) {
const ok = await isStreamPlayable(streamData.url, customHeaders);
if (!ok) streamData.url = null;
}
if (!streamData.url) {
return res.status(404).json({ error: 'No playable stream found' });
}
res.json({ success: true, provider, stream: streamData });
} catch (err) {
res.status(404).json({ error: err.message });
}
});
app.get('/api/subtitles', async (req, res) => {
const { tmdbId, season, episode } = req.query;
if (!tmdbId) return res.json({ subtitles: [] });
try {
const s = season && season !== 'null' && season !== 'undefined' ? season : undefined;
const e = episode && episode !== 'null' && episode !== 'undefined' ? episode : undefined;
const subtitles = await sdk.getSubtitles(String(tmdbId), s, e);
res.json({ subtitles: subtitles || [] });
} catch (err) {
res.json({ subtitles: [] });
}
});
app.get('/api/timestamps', async (req, res) => {
const { tmdb_id, season, episode, duration_ms } = req.query;
if (!tmdb_id) return res.status(400).json({ error: 'Missing tmdb_id' });
let u = `https://api.theintrodb.org/v3/media?tmdb_id=${tmdb_id}`;
if (season) u += `&season=${season}`;
if (episode) u += `&episode=${episode}`;
if (duration_ms) u += `&duration_ms=${duration_ms}`;
try {
const upstream = await fetch(u);
if (!upstream.ok) return res.json({ segments: [] });
const data = await upstream.json();
const segments = [];
['recap', 'intro', 'preview', 'credits'].forEach(type => {
if (Array.isArray(data[type])) {
data[type].forEach(seg => {
segments.push({
type,
label: type === 'credits' ? 'Skip Credits' : type === 'recap' ? 'Skip Recap' : type === 'preview' ? 'Skip Preview' : 'Skip Intro',
start: seg.start_ms != null ? seg.start_ms / 1000 : 0,
end: seg.end_ms != null ? seg.end_ms / 1000 : null
});
});
}
});
res.json({ segments });
} catch (_) {
res.json({ segments: [] });
}
});
app.all('/api/proxy', async (req, res) => {
const targetUrl = req.query.url;
if (!targetUrl) return res.status(400).send('Missing url parameter');
let customHeaders = {};
if (req.query.headers) {
try { customHeaders = JSON.parse(req.query.headers); } catch (_) { }
}
delete customHeaders['Host'];
delete customHeaders['host'];
const isStrip = req.query.strip === '1' || req.query.tt === '1' || /tiktokcdn\.com|ibyteimg\.com|seg\.html|enproxy|letsgocdn\d+\.shop/i.test(targetUrl);
try {
const fetchOpts = {
method: req.method === 'HEAD' ? 'HEAD' : 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9',
...customHeaders
},
redirect: 'follow'
};
if (req.headers.range) fetchOpts.headers['Range'] = req.headers.range;
let response;
let lastErr;
for (let i = 0; i < 3; i++) {
try {
response = await fetch(targetUrl, fetchOpts);
if (response.ok || response.status === 206 || response.status === 404) break;
if (response.status === 403) {
if (i === 2) break;
await new Promise(r => setTimeout(r, 1000));
continue;
}
} catch (err) {
lastErr = err;
if (i === 2) break;
await new Promise(r => setTimeout(r, 500 * (i + 1)));
}
}
if (!response) {
return res.status(502).send(lastErr ? lastErr.message : 'Bad Gateway');
}
res.set({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Headers': '*',
'Access-Control-Expose-Headers': 'Content-Length, Content-Range, Accept-Ranges'
});
if (req.method === 'OPTIONS') return res.sendStatus(204);
if (!response.ok && response.status !== 206) {
return res.status(response.status).send(`Upstream error: ${response.status}`);
}
const ct = (response.headers.get('content-type') || '').toLowerCase();
const isM3U8 = targetUrl.includes('.m3u8') || ct.includes('mpegurl') || ct.includes('m3u8');
if (targetUrl.includes('.srt') || ct.includes('subrip')) {
const raw = await response.text();
let vtt = 'WEBVTT\n\n' + raw
.replace(/\r\n|\r/g, '\n')
.replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2');
res.set('Content-Type', 'text/vtt; charset=utf-8');
return res.send(vtt);
}
if (isM3U8) {
const text = await response.text();
const baseUrl = targetUrl.substring(0, targetUrl.lastIndexOf('/') + 1);
const originBase = new URL(targetUrl).origin;
const headersParam = Object.keys(customHeaders).length ? `&headers=${encodeURIComponent(JSON.stringify(customHeaders))}` : '';
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const t = lines[i].trim();
if (!t) continue;
if (t.startsWith('#EXT-X-KEY') || t.startsWith('#EXT-X-MAP')) {
let uIdx = t.indexOf('URI="');
if (uIdx !== -1) {
const end = t.indexOf('"', uIdx + 5);
if (end !== -1) {
const uri = t.slice(uIdx + 5, end);
const abs = uri.startsWith('http') ? uri : uri.startsWith('/') ? originBase + uri : baseUrl + uri;
lines[i] = t.slice(0, uIdx + 5) + `/api/proxy?url=${encodeURIComponent(abs)}${headersParam}` + '"' + t.slice(end + 1);
}
}
} else if (!t.startsWith('#')) {
const abs = t.startsWith('http') ? t : t.startsWith('/') ? originBase + t : baseUrl + t;
const stripParam = isStrip || /seg\.html|enproxy|tiktokcdn|ibyteimg/i.test(abs) ? '&strip=1' : '';
lines[i] = `/api/proxy?url=${encodeURIComponent(abs)}${headersParam}${stripParam}`;
}
}
res.set('Content-Type', 'application/vnd.apple.mpegurl');
return res.send(lines.join('\n'));
}
if (response.headers.has('content-type')) res.set('Content-Type', response.headers.get('content-type'));
if (response.headers.has('content-length')) res.set('Content-Length', response.headers.get('content-length'));
if (response.headers.has('content-range')) res.set('Content-Range', response.headers.get('content-range'));
if (response.headers.has('accept-ranges')) res.set('Accept-Ranges', response.headers.get('accept-ranges'));
if (req.method === 'HEAD') return res.end();
if (isStrip) {
const arrayBuffer = await response.arrayBuffer();
let buffer = Buffer.from(arrayBuffer);
if (buffer.length > 120 && (buffer[0] === 0x89 || buffer[0] === 0xFF || buffer[0] === 0x00)) {
buffer = buffer.subarray(120);
}
res.set('Content-Type', 'video/MP2T');
res.removeHeader('content-length');
return res.send(buffer);
}
res.status(response.status);
if (response.body) {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(value);
}
res.end();
} else {
res.end();
}
} catch (err) {
res.status(502).send(err.message);
}
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});