try to direct play video first and fallback to hls if we encounter error.
This commit is contained in:
parent
259eae251a
commit
b7b72cf21e
8 changed files with 186 additions and 121 deletions
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
|
|
@ -21,7 +21,8 @@
|
||||||
"NUXT_API_URL": "http://localhost:8081/api/",
|
"NUXT_API_URL": "http://localhost:8081/api/",
|
||||||
"NUXT_PUBLIC_WSS": ":8081/",
|
"NUXT_PUBLIC_WSS": ":8081/",
|
||||||
},
|
},
|
||||||
"console": "internalConsole"
|
"console": "internalConsole",
|
||||||
|
"outputCapture": "std",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Python: main.py",
|
"name": "Python: main.py",
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ from .Utils import (
|
||||||
StreamingError,
|
StreamingError,
|
||||||
arg_converter,
|
arg_converter,
|
||||||
calc_download_path,
|
calc_download_path,
|
||||||
|
get_sidecar_subtitles,
|
||||||
get_video_info,
|
get_video_info,
|
||||||
validate_url,
|
validate_url,
|
||||||
validate_uuid,
|
validate_uuid,
|
||||||
|
|
@ -1178,6 +1179,46 @@ class HttpAPI(Common):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return web.json_response(data={"error": str(e)}, status=web.HTTPInternalServerError.status_code)
|
return web.json_response(data={"error": str(e)}, status=web.HTTPInternalServerError.status_code)
|
||||||
|
|
||||||
|
@route("GET", "api/file/info/{file:.*}")
|
||||||
|
async def get_file_info(self, request: Request) -> Response:
|
||||||
|
"""
|
||||||
|
Get file info
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Request): The request object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: The response object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
file: str = request.match_info.get("file")
|
||||||
|
if not file:
|
||||||
|
return web.json_response(data={"error": "file is required."}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
try:
|
||||||
|
realFile: str = calc_download_path(base_path=self.config.download_path, folder=file, create_path=False)
|
||||||
|
if not os.path.exists(realFile) or not os.path.isfile(realFile):
|
||||||
|
return web.json_response(
|
||||||
|
data={"error": f"File '{file}' does not exist."}, status=web.HTTPNotFound.status_code
|
||||||
|
)
|
||||||
|
|
||||||
|
realFile = Path(realFile)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"ffprobe": await ffprobe(realFile),
|
||||||
|
"sidecar": get_sidecar_subtitles(realFile),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, f in enumerate(response["sidecar"]):
|
||||||
|
response["sidecar"][i]["file"] = str(Path(realFile).with_name(f["file"].name)).replace(
|
||||||
|
self.config.download_path, ""
|
||||||
|
).strip("/")
|
||||||
|
|
||||||
|
return web.json_response(data=response, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
return web.json_response(data={"error": str(e)}, status=web.HTTPInternalServerError.status_code)
|
||||||
|
|
||||||
@route("GET", "api/youtube/auth")
|
@route("GET", "api/youtube/auth")
|
||||||
async def is_authenticated(self, request: Request) -> Response:
|
async def is_authenticated(self, request: Request) -> Response:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import glob
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from aiohttp.web import HTTPFound, Response
|
from aiohttp.web import HTTPFound, Response
|
||||||
|
|
||||||
from .ffprobe import ffprobe
|
from .ffprobe import ffprobe
|
||||||
from .Subtitle import Subtitle
|
from .Utils import StreamingError, calc_download_path, check_id, get_sidecar_subtitles
|
||||||
from .Utils import StreamingError, calc_download_path, check_id
|
|
||||||
|
|
||||||
|
|
||||||
class Playlist:
|
class Playlist:
|
||||||
|
|
@ -41,28 +38,19 @@ class Playlist:
|
||||||
msg = f"Unable to get '{rFile}' duration."
|
msg = f"Unable to get '{rFile}' duration."
|
||||||
raise StreamingError(msg)
|
raise StreamingError(msg)
|
||||||
|
|
||||||
duration: float = float(ff.metadata.get("duration"))
|
|
||||||
|
|
||||||
playlist = []
|
playlist = []
|
||||||
playlist.append("#EXTM3U")
|
playlist.append("#EXTM3U")
|
||||||
|
|
||||||
subs = ""
|
subs = ""
|
||||||
|
|
||||||
index = 0
|
duration: float = float(ff.metadata.get("duration"))
|
||||||
for item in self.get_sidecar_files(rFile):
|
for sub_file in get_sidecar_subtitles(rFile):
|
||||||
if item.suffix not in Subtitle.allowed_extensions:
|
lang = sub_file["lang"]
|
||||||
continue
|
item = sub_file["file"]
|
||||||
|
name = sub_file["name"]
|
||||||
index += 1
|
|
||||||
lang: str = "und"
|
|
||||||
lg = re.search(r"\.(?P<lang>\w{2,3})\.\w{3}$", item.name)
|
|
||||||
if lg:
|
|
||||||
lang = lg.groupdict().get("lang")
|
|
||||||
|
|
||||||
subs = ',SUBTITLES="subs"'
|
subs = ',SUBTITLES="subs"'
|
||||||
|
|
||||||
url = f"{self.url}api/player/m3u8/subtitle/{quote(str(Path(file).with_name(item.name)))}.m3u8?duration={duration}"
|
url = f"{self.url}api/player/m3u8/subtitle/{quote(str(Path(file).with_name(item.name)))}.m3u8?duration={duration}"
|
||||||
name = f"{item.suffix[1:].upper()} ({index}) - {lang}"
|
|
||||||
playlist.append(
|
playlist.append(
|
||||||
f'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="{name}",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE="{lang}",URI="{url}"'
|
f'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="{name}",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE="{lang}",URI="{url}"'
|
||||||
)
|
)
|
||||||
|
|
@ -71,20 +59,3 @@ class Playlist:
|
||||||
playlist.append(f"{self.url}api/player/m3u8/video/{quote(file)}.m3u8")
|
playlist.append(f"{self.url}api/player/m3u8/video/{quote(file)}.m3u8")
|
||||||
|
|
||||||
return "\n".join(playlist)
|
return "\n".join(playlist)
|
||||||
|
|
||||||
def get_sidecar_files(self, file: Path) -> list[Path]:
|
|
||||||
"""
|
|
||||||
Get sidecar files for the given file.
|
|
||||||
|
|
||||||
:param file: File to get sidecar files for.
|
|
||||||
:return: List of sidecar files.
|
|
||||||
"""
|
|
||||||
files = []
|
|
||||||
|
|
||||||
for sub_file in file.parent.glob(f"{glob.escape(file.stem)}.*"):
|
|
||||||
if sub_file == file or sub_file.is_file() is False or sub_file.stem.startswith("."):
|
|
||||||
continue
|
|
||||||
|
|
||||||
files.append(sub_file)
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import copy
|
import copy
|
||||||
|
import glob
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -27,6 +28,8 @@ IGNORED_KEYS: tuple[str] = (
|
||||||
)
|
)
|
||||||
YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None
|
YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None
|
||||||
|
|
||||||
|
ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass")
|
||||||
|
|
||||||
|
|
||||||
class StreamingError(Exception):
|
class StreamingError(Exception):
|
||||||
"""Raised when an error occurs during streaming."""
|
"""Raised when an error occurs during streaming."""
|
||||||
|
|
@ -524,3 +527,27 @@ def validate_uuid(uuid_str: str, version: int = 4) -> bool:
|
||||||
return True
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_sidecar_subtitles(file: pathlib.Path) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Get sidecar files for the given file.
|
||||||
|
|
||||||
|
:param file: File to get sidecar files for.
|
||||||
|
:return: List of sidecar files.
|
||||||
|
"""
|
||||||
|
files = []
|
||||||
|
|
||||||
|
for i, f in enumerate(file.parent.glob(f"{glob.escape(file.stem)}.*")):
|
||||||
|
if f == file or f.is_file() is False or f.stem.startswith("."):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if f.suffix not in ALLOWED_SUBS_EXTENSIONS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lg = re.search(r"\.(?P<lang>\w{2,3})\.\w{3}$", f.name)
|
||||||
|
lang = lg.groupdict().get("lang") if lg else "und"
|
||||||
|
|
||||||
|
files.append({"file": f, "lang": lang, "name": f"{f.suffix[1:].upper()} ({i}) - {lang}"})
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
class Encoder(json.JSONEncoder):
|
class Encoder(json.JSONEncoder):
|
||||||
|
|
@ -9,10 +10,14 @@ class Encoder(json.JSONEncoder):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
if isinstance(o, object) and hasattr(o, "serialize"):
|
if isinstance(o, Path):
|
||||||
return o.serialize()
|
return str(o)
|
||||||
|
|
||||||
if isinstance(o, object) and hasattr(o, "__dict__"):
|
if isinstance(o, object):
|
||||||
return o.__dict__
|
if hasattr(o, "serialize"):
|
||||||
|
return o.serialize()
|
||||||
|
|
||||||
|
if hasattr(o, "__dict__"):
|
||||||
|
return o.__dict__
|
||||||
|
|
||||||
return json.JSONEncoder.default(self, o)
|
return json.JSONEncoder.default(self, o)
|
||||||
|
|
|
||||||
|
|
@ -235,12 +235,11 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal is-active" v-if="video_link">
|
<div class="modal is-active" v-if="video_item">
|
||||||
<div class="modal-background" @click="closeVideo"></div>
|
<div class="modal-background" @click="closeVideo"></div>
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<VideoPlayer type="default" :link="video_link" :isMuted="false" autoplay="true" :isControls="true"
|
<VideoPlayer type="default" :isMuted="false" autoplay="true" :isControls="true" :item="video_item"
|
||||||
:title="video_title" :thumbnail="video_thumbnail" :artist="video_artist" class="is-fullwidth"
|
class="is-fullwidth" @closeModel="closeVideo" />
|
||||||
@closeModel="closeVideo" />
|
|
||||||
</div>
|
</div>
|
||||||
<button class="modal-close is-large" aria-label="close" @click="closeVideo"></button>
|
<button class="modal-close is-large" aria-label="close" @click="closeVideo"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -264,36 +263,10 @@ const showCompleted = useStorage('showCompleted', true)
|
||||||
const hideThumbnail = useStorage('hideThumbnailHistory', false)
|
const hideThumbnail = useStorage('hideThumbnailHistory', false)
|
||||||
const direction = useStorage('sortCompleted', 'desc')
|
const direction = useStorage('sortCompleted', 'desc')
|
||||||
|
|
||||||
const video_link = ref('')
|
const video_item = ref(null)
|
||||||
const video_title = ref('')
|
|
||||||
const video_thumbnail = ref('')
|
|
||||||
const video_artist = ref('')
|
|
||||||
|
|
||||||
const playVideo = item => {
|
const playVideo = item => video_item.value = item
|
||||||
video_thumbnail.value = '';
|
const closeVideo = () => video_item.value = null
|
||||||
video_artist.value = '';
|
|
||||||
video_link.value = makeDownload(config, item, 'm3u8')
|
|
||||||
video_title.value = item.title
|
|
||||||
if (item.extras?.thumbnail) {
|
|
||||||
video_thumbnail.value = '/api/thumbnail?url=' + encodePath(item.extras.thumbnail)
|
|
||||||
}
|
|
||||||
if (item.extras?.channel) {
|
|
||||||
video_artist.value = item.extras.channel
|
|
||||||
}
|
|
||||||
if (!video_artist.value && item.extras?.uploader) {
|
|
||||||
video_artist.value = item.extras.uploader
|
|
||||||
}
|
|
||||||
if (!item.extras?.is_video && item.extras?.is_audio) {
|
|
||||||
video_audio.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeVideo = () => {
|
|
||||||
video_link.value = ''
|
|
||||||
video_title.value = ''
|
|
||||||
video_thumbnail.value = ''
|
|
||||||
video_artist.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(masterSelectAll, (value) => {
|
watch(masterSelectAll, (value) => {
|
||||||
for (const key in stateStore.history) {
|
for (const key in stateStore.history) {
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,10 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<video ref="video" :poster="thumbnail" :controls="isControls" :title="title" playsinline>
|
<video id="player" ref="video" :poster="thumbnail" :title="title" playsinline>
|
||||||
<source :src="link" type="application/x-mpegURL" />
|
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror" />
|
||||||
|
<track v-for="track in tracks" :key="track.file" :kind="track.kind" :label="track.label" :srclang="track.lang"
|
||||||
|
:src="track.file" default />
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -29,33 +31,27 @@ import { onMounted, onUpdated, ref, defineProps, defineEmits, onUnmounted } from
|
||||||
import Hls from 'hls.js'
|
import Hls from 'hls.js'
|
||||||
import Plyr from 'plyr'
|
import Plyr from 'plyr'
|
||||||
import 'plyr/dist/plyr.css'
|
import 'plyr/dist/plyr.css'
|
||||||
|
import { makeDownload } from '~/utils/index'
|
||||||
|
const config = useConfigStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
link: {
|
item: {
|
||||||
type: String,
|
type: Object,
|
||||||
default: ''
|
default: () => ({})
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
thumbnail: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
artist: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
isControls: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emitter = defineEmits(['closeModel'])
|
const emitter = defineEmits(['closeModel'])
|
||||||
|
|
||||||
const video = ref(null)
|
const video = ref(null)
|
||||||
|
const tracks = ref([])
|
||||||
|
const sources = ref([])
|
||||||
|
|
||||||
|
const thumbnail = ref('')
|
||||||
|
const artist = ref('')
|
||||||
|
const title = ref('')
|
||||||
|
const isAudio = ref(false)
|
||||||
|
|
||||||
let player = null;
|
let player = null;
|
||||||
let hls = null;
|
let hls = null;
|
||||||
|
|
||||||
|
|
@ -65,7 +61,43 @@ const eventFunc = e => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
const response = await (await request(makeDownload(config, props.item, 'api/file/info'))).json()
|
||||||
|
|
||||||
|
if (props.item.extras?.thumbnail) {
|
||||||
|
thumbnail.value = '/api/thumbnail?url=' + encodePath(props.item.extras.thumbnail)
|
||||||
|
}
|
||||||
|
|
||||||
|
sources.value.push({
|
||||||
|
src: makeDownload(config, props.item, 'api/download'),
|
||||||
|
onerror: e => src_error(e),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (props.item.extras?.channel) {
|
||||||
|
artist.value = props.item.extras.channel
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!artist.value && props.item.extras?.uploader) {
|
||||||
|
artist.value = props.item.extras.uploader
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.item?.title) {
|
||||||
|
title.value = props.item.title
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.item.extras?.is_video && props.item.extras?.is_audio) {
|
||||||
|
isAudio.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
response.sidecar.forEach((cap, id) => {
|
||||||
|
tracks.value.push({
|
||||||
|
kind: "captions",
|
||||||
|
label: cap.name,
|
||||||
|
lang: cap.lang,
|
||||||
|
file: `${makeDownload(config, { filename: cap.file }, 'api/player/subtitle')}.vtt`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
if (/(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent)) {
|
if (/(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent)) {
|
||||||
document.documentElement.style.setProperty('--webkit-text-track-display', 'block');
|
document.documentElement.style.setProperty('--webkit-text-track-display', 'block');
|
||||||
}
|
}
|
||||||
|
|
@ -86,23 +118,23 @@ onUnmounted(() => {
|
||||||
|
|
||||||
window.removeEventListener('keydown', eventFunc)
|
window.removeEventListener('keydown', eventFunc)
|
||||||
|
|
||||||
if (props.title) {
|
if (title.value) {
|
||||||
window.document.title = 'YTPTube'
|
window.document.title = 'YTPTube'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const prepareVideoPlayer = () => {
|
const prepareVideoPlayer = () => {
|
||||||
let mediaMetadata = {
|
let mediaMetadata = {
|
||||||
title: props.title,
|
title: title.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.thumbnail) {
|
if (thumbnail.value) {
|
||||||
mediaMetadata['artwork'] = [
|
mediaMetadata['artwork'] = [
|
||||||
{ src: props.thumbnail, sizes: '1920x1080', type: 'image/jpeg' },
|
{ src: thumbnail.value, sizes: '1920x1080', type: 'image/jpeg' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
if (props.artist) {
|
if (artist.value) {
|
||||||
mediaMetadata['artist'] = props.artist
|
mediaMetadata['artist'] = artist.value
|
||||||
}
|
}
|
||||||
|
|
||||||
let opts = {
|
let opts = {
|
||||||
|
|
@ -121,28 +153,47 @@ const prepareVideoPlayer = () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
key: 'plyr'
|
key: 'plyr'
|
||||||
},
|
},
|
||||||
poster: props.thumbnail,
|
artist: artist.value,
|
||||||
artist: props.artist,
|
|
||||||
title: props.title,
|
|
||||||
mediaMetadata: mediaMetadata,
|
mediaMetadata: mediaMetadata,
|
||||||
captions: {
|
captions: {
|
||||||
update: true,
|
update: true,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.artist) {
|
if (artist.value) {
|
||||||
opts.artist = props.artist
|
opts.artist = artist.value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.title) {
|
if (title.value) {
|
||||||
opts.title = props.title
|
opts.title = title.value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.thumbnail) {
|
if (thumbnail.value) {
|
||||||
opts.poster = props.thumbnail
|
opts.poster = thumbnail.value
|
||||||
}
|
}
|
||||||
|
|
||||||
player = new Plyr(video.value, opts);
|
player = new Plyr(video.value, opts);
|
||||||
|
|
||||||
|
player.source = {
|
||||||
|
type: isAudio.value ? 'audio' : 'video',
|
||||||
|
title: title.value,
|
||||||
|
poster: thumbnail.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (title.value) {
|
||||||
|
window.document.title = `YTPTube - Playing: ${title.value}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const src_error = () => {
|
||||||
|
if (hls) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.warn('Direct play failed, trying HLS.');
|
||||||
|
attach_hls(makeDownload(config, props.item, 'm3u8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const attach_hls = link => {
|
||||||
hls = new Hls({
|
hls = new Hls({
|
||||||
debug: false,
|
debug: false,
|
||||||
enableWorker: true,
|
enableWorker: true,
|
||||||
|
|
@ -151,14 +202,7 @@ const prepareVideoPlayer = () => {
|
||||||
fragLoadingTimeOut: 200000,
|
fragLoadingTimeOut: 200000,
|
||||||
});
|
});
|
||||||
|
|
||||||
hls.loadSource(props.link)
|
hls.loadSource(link)
|
||||||
|
hls.attachMedia(video.value)
|
||||||
if (video.value) {
|
|
||||||
hls.attachMedia(video.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.title) {
|
|
||||||
window.document.title = `YTPTube - Playing: ${props.title}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -350,7 +350,10 @@ const getQueryParams = (url = window.location.search) => Object.fromEntries(new
|
||||||
* @returns {string} The download URL
|
* @returns {string} The download URL
|
||||||
*/
|
*/
|
||||||
const makeDownload = (config, item, base = 'api/download') => {
|
const makeDownload = (config, item, base = 'api/download') => {
|
||||||
let baseDir = 'api/download' === base ? `${base}/` : 'api/player/playlist/';
|
let baseDir = 'api/player/m3u8/video/';
|
||||||
|
if ('m3u8' !== base) {
|
||||||
|
baseDir = `${base}/`;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.folder) {
|
if (item.folder) {
|
||||||
item.folder = item.folder.replace(/#/g, '%23');
|
item.folder = item.folder.replace(/#/g, '%23');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue