mirror of
https://gitlab.com/vyla-entertainment/TV.git
synced 2026-11-09 17:47:20 -05:00
Initial commit
This commit is contained in:
31
android/app/src/main/AndroidManifest.xml
Normal file
31
android/app/src/main/AndroidManifest.xml
Normal 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
BIN
assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
9
capacitor.config.json
Normal file
9
capacitor.config.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"appId": "tv.vyla.app",
|
||||
"appName": "Vyla",
|
||||
"webDir": ".",
|
||||
"server": {
|
||||
"androidScheme": "https",
|
||||
"cleartext": true
|
||||
}
|
||||
}
|
||||
315
css/base.css
Normal file
315
css/base.css
Normal 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
432
css/detail.css
Normal 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
65
css/library.css
Normal 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
715
css/player.css
Normal 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
268
css/search.css
Normal 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
114
css/sidebar.css
Normal 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
213
css/skeleton.css
vendored
Normal 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
90
index.html
Normal 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">▶ Play</button><button class="ib f"
|
||||
id="hinfo">ⓘ 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
57
js/browse.js
Normal 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>★ ${(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
37
js/core.js
Normal 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, '<');
|
||||
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
66
js/detail.js
Normal 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">⋮</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>▶ ${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
60
js/focus.js
Normal 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
99
js/input.js
Normal 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
27
js/library.js
Normal 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
8
js/main.js
Normal 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
805
js/player.js
Normal 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
70
js/search.js
Normal 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>★ ${(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
16
js/sidebar.js
Normal 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
13
js/skeleton.js
Normal 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
14
node_modules/.package-lock.json
generated
vendored
Normal 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
1
node_modules/@vyla-entertainment/sdk/.env.example
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
TMDB_API_KEY=your_tmdb_api_key_here
|
||||
38
node_modules/@vyla-entertainment/sdk/.github/workflows/publish.yml
generated
vendored
Normal file
38
node_modules/@vyla-entertainment/sdk/.github/workflows/publish.yml
generated
vendored
Normal 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
16
node_modules/@vyla-entertainment/sdk/.gitlab-ci.yml
generated
vendored
Normal 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
407
node_modules/@vyla-entertainment/sdk/LICENSE
generated
vendored
Normal 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
31
node_modules/@vyla-entertainment/sdk/README.md
generated
vendored
Normal 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
33
node_modules/@vyla-entertainment/sdk/package.json
generated
vendored
Normal 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
487
node_modules/@vyla-entertainment/sdk/src/config.js
generated
vendored
Normal 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;
|
||||
53
node_modules/@vyla-entertainment/sdk/src/downloads/downloads.js
generated
vendored
Normal file
53
node_modules/@vyla-entertainment/sdk/src/downloads/downloads.js
generated
vendored
Normal 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
38
node_modules/@vyla-entertainment/sdk/src/health.js
generated
vendored
Normal 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
122
node_modules/@vyla-entertainment/sdk/src/sdk.d.ts
generated
vendored
Normal 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
68
node_modules/@vyla-entertainment/sdk/src/sdk.js
generated
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
101
node_modules/@vyla-entertainment/sdk/src/sources/111477.js
generated
vendored
Normal file
101
node_modules/@vyla-entertainment/sdk/src/sources/111477.js
generated
vendored
Normal 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,
|
||||
};
|
||||
}
|
||||
57
node_modules/@vyla-entertainment/sdk/src/sources/123anime.js
generated
vendored
Normal file
57
node_modules/@vyla-entertainment/sdk/src/sources/123anime.js
generated
vendored
Normal 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;
|
||||
}
|
||||
124
node_modules/@vyla-entertainment/sdk/src/sources/anihq.js
generated
vendored
Normal file
124
node_modules/@vyla-entertainment/sdk/src/sources/anihq.js
generated
vendored
Normal 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;
|
||||
}
|
||||
190
node_modules/@vyla-entertainment/sdk/src/sources/anineko.js
generated
vendored
Normal file
190
node_modules/@vyla-entertainment/sdk/src/sources/anineko.js
generated
vendored
Normal 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))] : [];
|
||||
}
|
||||
202
node_modules/@vyla-entertainment/sdk/src/sources/bcine.js
generated
vendored
Normal file
202
node_modules/@vyla-entertainment/sdk/src/sources/bcine.js
generated
vendored
Normal 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;
|
||||
}
|
||||
150
node_modules/@vyla-entertainment/sdk/src/sources/dulo.js
generated
vendored
Normal file
150
node_modules/@vyla-entertainment/sdk/src/sources/dulo.js
generated
vendored
Normal 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,
|
||||
};
|
||||
}
|
||||
118
node_modules/@vyla-entertainment/sdk/src/sources/flaxmovies.js
generated
vendored
Normal file
118
node_modules/@vyla-entertainment/sdk/src/sources/flaxmovies.js
generated
vendored
Normal 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'];
|
||||
}
|
||||
48
node_modules/@vyla-entertainment/sdk/src/sources/fsonline.js
generated
vendored
Normal file
48
node_modules/@vyla-entertainment/sdk/src/sources/fsonline.js
generated
vendored
Normal 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))] : [];
|
||||
}
|
||||
60
node_modules/@vyla-entertainment/sdk/src/sources/kisskh.js
generated
vendored
Normal file
60
node_modules/@vyla-entertainment/sdk/src/sources/kisskh.js
generated
vendored
Normal 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))] : [];
|
||||
}
|
||||
43
node_modules/@vyla-entertainment/sdk/src/sources/lmscript.js
generated
vendored
Normal file
43
node_modules/@vyla-entertainment/sdk/src/sources/lmscript.js
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
80
node_modules/@vyla-entertainment/sdk/src/sources/lookmovie.js
generated
vendored
Normal file
80
node_modules/@vyla-entertainment/sdk/src/sources/lookmovie.js
generated
vendored
Normal 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; }
|
||||
}
|
||||
117
node_modules/@vyla-entertainment/sdk/src/sources/movienight.js
generated
vendored
Normal file
117
node_modules/@vyla-entertainment/sdk/src/sources/movienight.js
generated
vendored
Normal 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,
|
||||
};
|
||||
}
|
||||
282
node_modules/@vyla-entertainment/sdk/src/sources/movy.js
generated
vendored
Normal file
282
node_modules/@vyla-entertainment/sdk/src/sources/movy.js
generated
vendored
Normal 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,
|
||||
};
|
||||
}
|
||||
50
node_modules/@vyla-entertainment/sdk/src/sources/pengu.js
generated
vendored
Normal file
50
node_modules/@vyla-entertainment/sdk/src/sources/pengu.js
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
68
node_modules/@vyla-entertainment/sdk/src/sources/pstream.js
generated
vendored
Normal file
68
node_modules/@vyla-entertainment/sdk/src/sources/pstream.js
generated
vendored
Normal 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;
|
||||
}
|
||||
104
node_modules/@vyla-entertainment/sdk/src/sources/purstream.js
generated
vendored
Normal file
104
node_modules/@vyla-entertainment/sdk/src/sources/purstream.js
generated
vendored
Normal 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
|
||||
};
|
||||
179
node_modules/@vyla-entertainment/sdk/src/sources/rivestream.js
generated
vendored
Normal file
179
node_modules/@vyla-entertainment/sdk/src/sources/rivestream.js
generated
vendored
Normal 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})`);
|
||||
}
|
||||
129
node_modules/@vyla-entertainment/sdk/src/sources/streamaggregator.js
generated
vendored
Normal file
129
node_modules/@vyla-entertainment/sdk/src/sources/streamaggregator.js
generated
vendored
Normal 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,
|
||||
};
|
||||
}
|
||||
115
node_modules/@vyla-entertainment/sdk/src/sources/vidapi.js
generated
vendored
Normal file
115
node_modules/@vyla-entertainment/sdk/src/sources/vidapi.js
generated
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
123
node_modules/@vyla-entertainment/sdk/src/sources/vidcore.js
generated
vendored
Normal file
123
node_modules/@vyla-entertainment/sdk/src/sources/vidcore.js
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
123
node_modules/@vyla-entertainment/sdk/src/sources/vidfast.js
generated
vendored
Normal file
123
node_modules/@vyla-entertainment/sdk/src/sources/vidfast.js
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
147
node_modules/@vyla-entertainment/sdk/src/sources/vidgod.js
generated
vendored
Normal file
147
node_modules/@vyla-entertainment/sdk/src/sources/vidgod.js
generated
vendored
Normal 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;
|
||||
}
|
||||
28
node_modules/@vyla-entertainment/sdk/src/sources/vidlink.js
generated
vendored
Normal file
28
node_modules/@vyla-entertainment/sdk/src/sources/vidlink.js
generated
vendored
Normal 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))] : [];
|
||||
}
|
||||
79
node_modules/@vyla-entertainment/sdk/src/sources/vidnest.js
generated
vendored
Normal file
79
node_modules/@vyla-entertainment/sdk/src/sources/vidnest.js
generated
vendored
Normal 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 };
|
||||
145
node_modules/@vyla-entertainment/sdk/src/sources/vidrock.js
generated
vendored
Normal file
145
node_modules/@vyla-entertainment/sdk/src/sources/vidrock.js
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
123
node_modules/@vyla-entertainment/sdk/src/sources/vidup.js
generated
vendored
Normal file
123
node_modules/@vyla-entertainment/sdk/src/sources/vidup.js
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
188
node_modules/@vyla-entertainment/sdk/src/sources/vidvault.js
generated
vendored
Normal file
188
node_modules/@vyla-entertainment/sdk/src/sources/vidvault.js
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
184
node_modules/@vyla-entertainment/sdk/src/sources/vidzee.js
generated
vendored
Normal file
184
node_modules/@vyla-entertainment/sdk/src/sources/vidzee.js
generated
vendored
Normal 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})`);
|
||||
}
|
||||
112
node_modules/@vyla-entertainment/sdk/src/sources/vixsrc.js
generated
vendored
Normal file
112
node_modules/@vyla-entertainment/sdk/src/sources/vixsrc.js
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
129
node_modules/@vyla-entertainment/sdk/src/sources/xdownloader.js
generated
vendored
Normal file
129
node_modules/@vyla-entertainment/sdk/src/sources/xdownloader.js
generated
vendored
Normal 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
122
node_modules/@vyla-entertainment/sdk/src/subtitles.js
generated
vendored
Normal 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 };
|
||||
230
node_modules/@vyla-entertainment/sdk/src/utils/helpers.js
generated
vendored
Normal file
230
node_modules/@vyla-entertainment/sdk/src/utils/helpers.js
generated
vendored
Normal 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
22
package-lock.json
generated
Normal 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
25
package.json
Normal 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
348
server.js
Normal 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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user