Merge pull request #240 from arabcoders/dev
Small improvements to file browser.
This commit is contained in:
commit
80737f7d90
12 changed files with 320 additions and 58 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -23,6 +23,7 @@
|
||||||
"daterange",
|
"daterange",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"finaldir",
|
"finaldir",
|
||||||
|
"flac",
|
||||||
"getpid",
|
"getpid",
|
||||||
"gpac",
|
"gpac",
|
||||||
"httpx",
|
"httpx",
|
||||||
|
|
@ -36,6 +37,7 @@
|
||||||
"muxdelay",
|
"muxdelay",
|
||||||
"nodesc",
|
"nodesc",
|
||||||
"noprogress",
|
"noprogress",
|
||||||
|
"plexmatch",
|
||||||
"postprocessor",
|
"postprocessor",
|
||||||
"preferredcodec",
|
"preferredcodec",
|
||||||
"preferredquality",
|
"preferredquality",
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,9 @@ from .Utils import (
|
||||||
encrypt_data,
|
encrypt_data,
|
||||||
extract_info,
|
extract_info,
|
||||||
get_file,
|
get_file,
|
||||||
|
get_file_sidecar,
|
||||||
get_files,
|
get_files,
|
||||||
get_mime_type,
|
get_mime_type,
|
||||||
get_sidecar_subtitles,
|
|
||||||
validate_url,
|
validate_url,
|
||||||
validate_uuid,
|
validate_uuid,
|
||||||
)
|
)
|
||||||
|
|
@ -1550,15 +1550,19 @@ class HttpAPI(Common):
|
||||||
ff_info = await ffprobe(realFile)
|
ff_info = await ffprobe(realFile)
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
|
"title": str(Path(realFile).stem),
|
||||||
"ffprobe": ff_info,
|
"ffprobe": ff_info,
|
||||||
"mimetype": get_mime_type(ff_info.get("metadata", {}), realFile),
|
"mimetype": get_mime_type(ff_info.get("metadata", {}), realFile),
|
||||||
"sidecar": get_sidecar_subtitles(realFile),
|
"sidecar": get_file_sidecar(realFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, f in enumerate(response["sidecar"]):
|
for key in response["sidecar"]:
|
||||||
response["sidecar"][i]["file"] = (
|
for i, f in enumerate(response["sidecar"][key]):
|
||||||
str(Path(realFile).with_name(f["file"].name)).replace(self.config.download_path, "").strip("/")
|
response["sidecar"][key][i]["file"] = (
|
||||||
)
|
str(Path(realFile).with_name(Path(f["file"]).name))
|
||||||
|
.replace(self.config.download_path, "")
|
||||||
|
.strip("/")
|
||||||
|
)
|
||||||
|
|
||||||
return web.json_response(data=response, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
|
return web.json_response(data=response, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from pathlib import Path
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from .ffprobe import ffprobe
|
from .ffprobe import ffprobe
|
||||||
from .Utils import StreamingError, get_sidecar_subtitles
|
from .Utils import StreamingError, get_file_sidecar
|
||||||
|
|
||||||
|
|
||||||
class Playlist:
|
class Playlist:
|
||||||
|
|
@ -30,9 +30,9 @@ class Playlist:
|
||||||
subs = ""
|
subs = ""
|
||||||
|
|
||||||
duration: float = float(ff.metadata.get("duration"))
|
duration: float = float(ff.metadata.get("duration"))
|
||||||
for sub_file in get_sidecar_subtitles(file):
|
for sub_file in get_file_sidecar(file).get("subtitle", []):
|
||||||
lang = sub_file["lang"]
|
lang = sub_file["lang"]
|
||||||
item = sub_file["file"]
|
item = Path(sub_file["file"])
|
||||||
name = sub_file["name"]
|
name = sub_file["name"]
|
||||||
|
|
||||||
subs = ',SUBTITLES="subs"'
|
subs = ',SUBTITLES="subs"'
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,15 @@ YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None
|
||||||
|
|
||||||
ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass")
|
ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass")
|
||||||
|
|
||||||
|
FILES_TYPE: list = [
|
||||||
|
{"rx": re.compile(r"\.(avi|ts|mkv|mp4|mp3|mpv|ogm|m4v|webm|m4b)$", re.IGNORECASE), "type": "video"},
|
||||||
|
{"rx": re.compile(r"\.(mp3|flac|aac|opus|wav|m4a)$", re.IGNORECASE), "type": "audio"},
|
||||||
|
{"rx": re.compile(r"\.(srt|ass|ssa|smi|sub|idx)$", re.IGNORECASE), "type": "subtitle"},
|
||||||
|
{"rx": re.compile(r"\.(jpg|jpeg|png|gif|bmp|webp)$", re.IGNORECASE), "type": "image"},
|
||||||
|
{"rx": re.compile(r"\.(txt|nfo|md|json|yml|yaml|plexmatch)$", re.IGNORECASE), "type": "text"},
|
||||||
|
{"rx": re.compile(r"\.(nfo|json|jpg|torrent|\.info\.json)$", re.IGNORECASE), "type": "metadata"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class StreamingError(Exception):
|
class StreamingError(Exception):
|
||||||
"""Raised when an error occurs during streaming."""
|
"""Raised when an error occurs during streaming."""
|
||||||
|
|
@ -480,7 +489,7 @@ def validate_uuid(uuid_str: str, version: int = 4) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_sidecar_subtitles(file: pathlib.Path) -> list[dict]:
|
def get_file_sidecar(file: pathlib.Path) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Get sidecar files for the given file.
|
Get sidecar files for the given file.
|
||||||
|
|
||||||
|
|
@ -491,26 +500,60 @@ def get_sidecar_subtitles(file: pathlib.Path) -> list[dict]:
|
||||||
list: List of sidecar files.
|
list: List of sidecar files.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
files = []
|
files = {}
|
||||||
|
|
||||||
for i, f in enumerate(file.parent.glob(f"{glob.escape(file.stem)}.*")):
|
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("."):
|
if f == file or f.is_file() is False or f.stem.startswith("."):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if f.suffix not in ALLOWED_SUBS_EXTENSIONS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if f.stat().st_size < 1:
|
if f.stat().st_size < 1:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lg = re.search(r"\.(?P<lang>\w{2,3})\.\w{3}$", f.name)
|
content_type = "Unknown"
|
||||||
lang = lg.groupdict().get("lang") if lg else "und"
|
for pattern in FILES_TYPE:
|
||||||
|
if pattern["rx"].search(f.name):
|
||||||
|
content_type = pattern["type"]
|
||||||
|
break
|
||||||
|
|
||||||
files.append({"file": f, "lang": lang, "name": f"{f.suffix[1:].upper()} ({i}) - {lang}"})
|
if content_type == "subtitle":
|
||||||
|
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"
|
||||||
|
content = {"file": f, "lang": lang, "name": f"{f.suffix[1:].upper()} ({i}) - {lang}"}
|
||||||
|
else:
|
||||||
|
content = {"file": f}
|
||||||
|
|
||||||
|
if content_type not in files:
|
||||||
|
files[content_type] = []
|
||||||
|
|
||||||
|
files[content_type].append(content)
|
||||||
|
|
||||||
|
images = get_possible_images(str(file.parent))
|
||||||
|
if len(images) > 0:
|
||||||
|
if "image" not in files:
|
||||||
|
files["image"] = []
|
||||||
|
|
||||||
|
files["image"].extend(images)
|
||||||
|
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=512)
|
||||||
|
def get_possible_images(dir: str) -> list[dict]:
|
||||||
|
images = []
|
||||||
|
|
||||||
|
path_loc = pathlib.Path(dir, "test.jpg")
|
||||||
|
|
||||||
|
for filename in ["poster", "thumbnail", "artwork", "cover", "fanart"]:
|
||||||
|
for ext in [".jpg", ".jpeg", ".png", ".webp"]:
|
||||||
|
f = path_loc.with_stem(filename).with_suffix(ext)
|
||||||
|
if f.exists():
|
||||||
|
images.append({"file": f})
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
def get_mime_type(metadata: dict, file_path: pathlib.Path) -> str:
|
def get_mime_type(metadata: dict, file_path: pathlib.Path) -> str:
|
||||||
"""
|
"""
|
||||||
Determine the correct MIME type for a video file based on ffprobe metadata.
|
Determine the correct MIME type for a video file based on ffprobe metadata.
|
||||||
|
|
@ -738,13 +781,28 @@ def get_files(base_path: str, dir: str | None = None):
|
||||||
if file.name.startswith(".") or file.name.startswith("_"):
|
if file.name.startswith(".") or file.name.startswith("_"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
content_type = None
|
||||||
|
|
||||||
|
for pattern in FILES_TYPE:
|
||||||
|
if pattern["rx"].search(file.name):
|
||||||
|
content_type = pattern["type"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if not content_type and file.is_dir():
|
||||||
|
content_type = "dir"
|
||||||
|
|
||||||
|
if not content_type:
|
||||||
|
content_type = "download"
|
||||||
|
|
||||||
stat = file.stat()
|
stat = file.stat()
|
||||||
contents.append(
|
contents.append(
|
||||||
{
|
{
|
||||||
"type": "file" if file.is_file() else "dir",
|
"type": "file" if file.is_file() else "dir",
|
||||||
|
"content_type": content_type,
|
||||||
"name": file.name,
|
"name": file.name,
|
||||||
"path": str(file).replace(base_path, "").strip("/"),
|
"path": str(file).replace(base_path, "").strip("/"),
|
||||||
"size": stat.st_size,
|
"size": stat.st_size,
|
||||||
|
"mime": get_mime_type({}, file) if file.is_file() else "directory",
|
||||||
"mtime": datetime.datetime.fromtimestamp(stat.st_mtime, tz=datetime.UTC).isoformat(),
|
"mtime": datetime.datetime.fromtimestamp(stat.st_mtime, tz=datetime.UTC).isoformat(),
|
||||||
"ctime": datetime.datetime.fromtimestamp(stat.st_ctime, tz=datetime.UTC).isoformat(),
|
"ctime": datetime.datetime.fromtimestamp(stat.st_ctime, tz=datetime.UTC).isoformat(),
|
||||||
"is_dir": file.is_dir(),
|
"is_dir": file.is_dir(),
|
||||||
|
|
|
||||||
|
|
@ -281,3 +281,8 @@ hr {
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-blend-mode: darken;
|
background-blend-mode: darken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-unbounded-model {
|
||||||
|
max-height: unset !important;
|
||||||
|
width: unset !important;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
|
<style scoped>
|
||||||
|
.embed-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 80vh;
|
||||||
|
width: 80vw;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<template>
|
<template>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1 class="has-text-white">Not downloaded yet.</h1>
|
<h1 class="has-text-white">Not downloaded yet.</h1>
|
||||||
<figure class="image is-16by9">
|
<iframe class="embed-content" :src="url" frameborder="0" allowfullscreen />
|
||||||
<iframe class="has-ratio" :src="url" frameborder="0" allowfullscreen />
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="modal is-active">
|
<div class="modal is-active" v-if="false === externalModel">
|
||||||
<div class="modal-background" @click="closeModal"></div>
|
<div class="modal-background" @click="closeModal"></div>
|
||||||
<div class="modal-content" style="width:60vw;">
|
<div class="modal-content" style="width:60vw;">
|
||||||
<div style="font-size:30vh; width: 99%" class="has-text-centered" v-if="isLoading">
|
<div style="font-size:30vh; width: 99%" class="has-text-centered" v-if="isLoading">
|
||||||
|
|
@ -20,6 +20,17 @@
|
||||||
</div>
|
</div>
|
||||||
<button class="modal-close is-large" aria-label="close" @click="closeModal"></button>
|
<button class="modal-close is-large" aria-label="close" @click="closeModal"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="width:70vw; height: 80vh;" v-else>
|
||||||
|
<div class="p-0 m-0" style="position: relative">
|
||||||
|
<div class="content" style="white-space: pre;">
|
||||||
|
<code class="p-4 is-block" v-text="data" />
|
||||||
|
<button class="button is-small m-4" @click="() => copyText(JSON.stringify(data, null, 4))"
|
||||||
|
style="position: absolute; top:0; right:0;">
|
||||||
|
<span class="icon"><i class="fas fa-copy"></i></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -34,7 +45,18 @@ const toast = useToast()
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
link: {
|
link: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: '',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
useUrl: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
externalModel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -49,12 +71,19 @@ const eventFunc = e => {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.addEventListener('keydown', eventFunc)
|
window.addEventListener('keydown', eventFunc)
|
||||||
|
|
||||||
const url = '/api/yt-dlp/url/info?url=' + encodePath(props.link)
|
const url = props.useUrl ? props.link : '/api/yt-dlp/url/info?url=' + encodePath(props.link)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
const response = await request(url, { credentials: 'include' });
|
const response = await request(url, { credentials: 'include' })
|
||||||
data.value = await response.json()
|
const body = await response.text()
|
||||||
|
|
||||||
|
try {
|
||||||
|
data.value = JSON.parse(body)
|
||||||
|
} catch (e) {
|
||||||
|
data.value = body
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.error(`Error: ${e.message}`)
|
toast.error(`Error: ${e.message}`)
|
||||||
|
|
|
||||||
|
|
@ -350,7 +350,7 @@
|
||||||
|
|
||||||
<div class="modal is-active" v-if="video_item">
|
<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 is-unbounded-model">
|
||||||
<VideoPlayer type="default" :isMuted="false" autoplay="true" :isControls="true" :item="video_item"
|
<VideoPlayer type="default" :isMuted="false" autoplay="true" :isControls="true" :item="video_item"
|
||||||
class="is-fullwidth" @closeModel="closeVideo" />
|
class="is-fullwidth" @closeModel="closeVideo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -359,7 +359,7 @@
|
||||||
|
|
||||||
<div class="modal is-active" v-if="embed_url">
|
<div class="modal is-active" v-if="embed_url">
|
||||||
<div class="modal-background" @click="embed_url = ''"></div>
|
<div class="modal-background" @click="embed_url = ''"></div>
|
||||||
<div class="modal-content">
|
<div class="modal-content is-unbounded-model">
|
||||||
<EmbedPlayer :url="embed_url" @closeModel="embed_url = ''" />
|
<EmbedPlayer :url="embed_url" @closeModel="embed_url = ''" />
|
||||||
</div>
|
</div>
|
||||||
<button class="modal-close is-large" aria-label="close" @click="embed_url = ''"></button>
|
<button class="modal-close is-large" aria-label="close" @click="embed_url = ''"></button>
|
||||||
|
|
@ -370,7 +370,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { makeDownload, formatBytes, ucFirst } from '~/utils/index'
|
import { makeDownload, formatBytes } from '~/utils/index'
|
||||||
import toast from '~/plugins/toast'
|
import toast from '~/plugins/toast'
|
||||||
import { isEmbedable, getEmbedable } from '~/utils/embedable'
|
import { isEmbedable, getEmbedable } from '~/utils/embedable'
|
||||||
|
|
||||||
|
|
|
||||||
66
ui/components/ImageView.vue
Normal file
66
ui/components/ImageView.vue
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<style>
|
||||||
|
.is-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:30vh; width: 99%" class="has-text-centered" v-if="isLoading">
|
||||||
|
<i class="fas fa-circle-notch fa-spin"></i>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<img :src="image" class="is-image">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { request } from '~/utils/index'
|
||||||
|
|
||||||
|
const emitter = defineEmits(['closeModel'])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const data = ref({})
|
||||||
|
const toast = useToast()
|
||||||
|
const image = ref('')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
link: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const eventFunc = e => {
|
||||||
|
if ('Escape' === e.key) {
|
||||||
|
emitter('closeModel')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
window.addEventListener('keydown', eventFunc)
|
||||||
|
|
||||||
|
|
||||||
|
const url = props.link.startsWith('/') ? props.link : '/api/thumbnail?url=' + encodePath(props.link)
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
const imgRequest = await request(url, { credentials: 'include' })
|
||||||
|
if (200 !== imgRequest.status) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
image.value = URL.createObjectURL(await imgRequest.blob())
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error(`Error: ${e.message}`)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
|
||||||
|
</script>
|
||||||
|
|
@ -137,7 +137,7 @@
|
||||||
</header>
|
</header>
|
||||||
<div v-if="false === hideThumbnail" class="card-image">
|
<div v-if="false === hideThumbnail" class="card-image">
|
||||||
<figure class="image is-3by1">
|
<figure class="image is-3by1">
|
||||||
<span v-if="isEmbedable(item.url)" @click="() => embed_url = getEmbedable(item.url)" class="play-overlay">
|
<span v-if="isEmbedable(item.url)" @click="embed_url = getEmbedable(item.url)" class="play-overlay">
|
||||||
<div class="play-icon embed-icon"></div>
|
<div class="play-icon embed-icon"></div>
|
||||||
<img @load="e => pImg(e)" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
|
<img @load="e => pImg(e)" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
|
||||||
v-if="item.extras?.thumbnail" />
|
v-if="item.extras?.thumbnail" />
|
||||||
|
|
@ -224,7 +224,7 @@
|
||||||
|
|
||||||
<div class="modal is-active" v-if="embed_url">
|
<div class="modal is-active" v-if="embed_url">
|
||||||
<div class="modal-background" @click="embed_url = ''"></div>
|
<div class="modal-background" @click="embed_url = ''"></div>
|
||||||
<div class="modal-content">
|
<div class="modal-content is-unbounded-model">
|
||||||
<EmbedPlayer :url="embed_url" @closeModel="embed_url = ''" />
|
<EmbedPlayer :url="embed_url" @closeModel="embed_url = ''" />
|
||||||
</div>
|
</div>
|
||||||
<button class="modal-close is-large" aria-label="close" @click="embed_url = ''"></button>
|
<button class="modal-close is-large" aria-label="close" @click="embed_url = ''"></button>
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: auto;
|
||||||
width: 100%;
|
max-height: 80vh;
|
||||||
|
max-width: 80vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<template>
|
<template>
|
||||||
<div v-if="infoLoaded">
|
<div v-if="infoLoaded">
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
|
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
|
||||||
:type="source.type" />
|
:type="source.type" />
|
||||||
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label"
|
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label"
|
||||||
:srclang="track.lang" :src="track.file" :default="notFirefox && i === 0" />
|
:srclang="track.lang" :src="track.file" :default="notFirefox && 0 === i" />
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center;" v-else>
|
<div style="text-align: center;" v-else>
|
||||||
|
|
@ -55,16 +55,16 @@ const volume = useStorage('player_volume', 1)
|
||||||
const notFirefox = !navigator.userAgent.toLowerCase().includes('firefox')
|
const notFirefox = !navigator.userAgent.toLowerCase().includes('firefox')
|
||||||
const infoLoaded = ref(false)
|
const infoLoaded = ref(false)
|
||||||
|
|
||||||
let hls = null;
|
let hls = null
|
||||||
|
|
||||||
const eventFunc = e => {
|
const eventFunc = e => {
|
||||||
if (e.key === 'Escape') {
|
if ('Escape' === e.key) {
|
||||||
emitter('closeModel')
|
emitter('closeModel')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const req = await request(makeDownload(config, props.item, 'api/file/info'));
|
const req = await request(makeDownload(config, props.item, 'api/file/info'))
|
||||||
|
|
||||||
const response = await req.json()
|
const response = await req.json()
|
||||||
|
|
||||||
|
|
@ -74,17 +74,14 @@ onMounted(async () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
infoLoaded.value = true
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
video.value.volume = volume.value
|
|
||||||
|
|
||||||
video.value.addEventListener('volumechange', () => {
|
|
||||||
volume.value = video.value.volume
|
|
||||||
})
|
|
||||||
|
|
||||||
if (props.item.extras?.thumbnail) {
|
if (props.item.extras?.thumbnail) {
|
||||||
thumbnail.value = '/api/thumbnail?url=' + encodePath(props.item.extras.thumbnail)
|
thumbnail.value = '/api/thumbnail?url=' + encodePath(props.item.extras.thumbnail)
|
||||||
|
} else {
|
||||||
|
if (response?.sidecar?.image && response.sidecar.image.length > 0) {
|
||||||
|
thumbnail.value = makeDownload(config, { "filename": response.sidecar.image[0]['file'] })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- check if mimetype is video/mp4 and device is apple
|
// -- check if mimetype is video/mp4 and device is apple
|
||||||
|
|
@ -115,12 +112,17 @@ onMounted(async () => {
|
||||||
if (props.item?.title) {
|
if (props.item?.title) {
|
||||||
title.value = props.item.title
|
title.value = props.item.title
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
if (response?.title) {
|
||||||
|
title.value = response.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!props.item.extras?.is_video && props.item.extras?.is_audio) {
|
if (!props.item.extras?.is_video && props.item.extras?.is_audio) {
|
||||||
isAudio.value = true
|
isAudio.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
response.sidecar.forEach((cap, id) => {
|
response?.sidecar?.subtitle?.forEach((cap, id) => {
|
||||||
tracks.value.push({
|
tracks.value.push({
|
||||||
kind: "captions",
|
kind: "captions",
|
||||||
label: cap.name,
|
label: cap.name,
|
||||||
|
|
@ -130,9 +132,12 @@ onMounted(async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isApple) {
|
if (isApple) {
|
||||||
document.documentElement.style.setProperty('--webkit-text-track-display', 'block');
|
document.documentElement.style.setProperty('--webkit-text-track-display', 'block')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
infoLoaded.value = true
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
prepareVideoPlayer()
|
prepareVideoPlayer()
|
||||||
window.addEventListener('keydown', eventFunc)
|
window.addEventListener('keydown', eventFunc)
|
||||||
})
|
})
|
||||||
|
|
@ -162,7 +167,7 @@ const prepareVideoPlayer = () => {
|
||||||
|
|
||||||
let mediaMetadata = {
|
let mediaMetadata = {
|
||||||
title: title.value,
|
title: title.value,
|
||||||
};
|
}
|
||||||
|
|
||||||
if (thumbnail.value) {
|
if (thumbnail.value) {
|
||||||
mediaMetadata.artwork = [{ src: thumbnail.value, sizes: '1920x1080', type: 'image/jpeg' }]
|
mediaMetadata.artwork = [{ src: thumbnail.value, sizes: '1920x1080', type: 'image/jpeg' }]
|
||||||
|
|
@ -172,18 +177,24 @@ const prepareVideoPlayer = () => {
|
||||||
mediaMetadata.artist = artist.value
|
mediaMetadata.artist = artist.value
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.mediaSession.metadata = new MediaMetadata(mediaMetadata);
|
navigator.mediaSession.metadata = new MediaMetadata(mediaMetadata)
|
||||||
if (title.value) {
|
if (title.value) {
|
||||||
window.document.title = `YTPTube - Playing: ${title.value}`
|
window.document.title = `YTPTube - Playing: ${title.value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
video.value.volume = volume.value
|
||||||
|
|
||||||
|
video.value.addEventListener('volumechange', () => {
|
||||||
|
volume.value = video.value.volume
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const src_error = () => {
|
const src_error = () => {
|
||||||
if (hls) {
|
if (hls) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.warn('Direct play failed, trying HLS.');
|
console.warn('Direct play failed, trying HLS.')
|
||||||
attach_hls(makeDownload(config, props.item, 'm3u8'));
|
attach_hls(makeDownload(config, props.item, 'm3u8'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const attach_hls = link => {
|
const attach_hls = link => {
|
||||||
|
|
@ -193,7 +204,7 @@ const attach_hls = link => {
|
||||||
lowLatencyMode: true,
|
lowLatencyMode: true,
|
||||||
backBufferLength: 120,
|
backBufferLength: 120,
|
||||||
fragLoadingTimeOut: 200000,
|
fragLoadingTimeOut: 200000,
|
||||||
});
|
})
|
||||||
|
|
||||||
hls.loadSource(link)
|
hls.loadSource(link)
|
||||||
hls.attachMedia(video.value)
|
hls.attachMedia(video.value)
|
||||||
|
|
|
||||||
|
|
@ -52,15 +52,30 @@
|
||||||
<td class="has-text-centered is-vcentered">
|
<td class="has-text-centered is-vcentered">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-solid"
|
<i class="fas fa-solid"
|
||||||
:class="{ 'fa-file': item.type === 'file', 'fa-folder': item.type === 'dir' }" />
|
:class="{ 'fa-file': 'file' === item.type, 'fa-folder': 'dir' === item.type }" />
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="is-text-overflow is-vcentered" v-tooltip="item.name">
|
<td class="is-text-overflow is-vcentered">
|
||||||
<a :href="`/browser/${item.path}`" v-if="item.type === 'dir'"
|
<div class="field is-grouped">
|
||||||
@click.prevent="reloadContent(item.path)">
|
<div class="control is-text-overflow is-expanded" v-tooltip="item.name">
|
||||||
{{ item.name }}
|
<a :href="`/browser/${item.path}`" v-if="'dir' === item.type"
|
||||||
</a>
|
@click.prevent="reloadContent(item.path)">
|
||||||
<a :href="makeDownload({}, { filename: item.path, folder: '' })" v-else>{{ item.name }}</a>
|
{{ item.name }}
|
||||||
|
</a>
|
||||||
|
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
|
||||||
|
@click.prevent="handleClick(item)" v-else>
|
||||||
|
{{ item.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="control" v-if="'file' === item.type">
|
||||||
|
<span class="icon">
|
||||||
|
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
|
||||||
|
:download="item.name.split('/').reverse()[0]">
|
||||||
|
<i class="fas fa-download" />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="has-text-centered is-text-overflow is-unselectable">
|
<td class="has-text-centered is-text-overflow is-unselectable">
|
||||||
{{ 'file' === item.type ? formatBytes(item.size) : 'Dir' }}
|
{{ 'file' === item.type ? formatBytes(item.size) : 'Dir' }}
|
||||||
|
|
@ -90,12 +105,24 @@
|
||||||
</Message>
|
</Message>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal is-active" v-if="model_item">
|
||||||
|
<div class="modal-background" @click="closeModel"></div>
|
||||||
|
<div class="modal-content is-unbounded-model">
|
||||||
|
<VideoPlayer type="default" :isMuted="false" autoplay="true" :isControls="true" :item="model_item"
|
||||||
|
class="is-fullwidth" @closeModel="closeModel" v-if="'video' === model_item.type" />
|
||||||
|
<GetInfo :link="model_item.filename" :useUrl="true" @closeModel="closeModel" :externalModel="true"
|
||||||
|
v-if="'text' === model_item.type" />
|
||||||
|
<ImageView :link="model_item.filename" @closeModel="closeModel" v-if="'image' === model_item.type" />
|
||||||
|
</div>
|
||||||
|
<button class="modal-close is-large" aria-label="close" @click="closeModel"></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { request } from '~/utils/index'
|
import { request } from '~/utils/index'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
@ -107,6 +134,12 @@ const initialLoad = ref(true)
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const path = ref(`/${route.params.slug?.length > 0 ? route.params.slug?.join('/') : ''}`)
|
const path = ref(`/${route.params.slug?.length > 0 ? route.params.slug?.join('/') : ''}`)
|
||||||
|
|
||||||
|
const bg_enable = useStorage('random_bg', true)
|
||||||
|
const bg_opacity = useStorage('random_bg_opacity', 0.85)
|
||||||
|
|
||||||
|
const model_item = ref()
|
||||||
|
const closeModel = () => model_item.value = null
|
||||||
|
|
||||||
watch(() => config.app.basic_mode, async () => {
|
watch(() => config.app.basic_mode, async () => {
|
||||||
if (!config.app.basic_mode) {
|
if (!config.app.basic_mode) {
|
||||||
return
|
return
|
||||||
|
|
@ -128,6 +161,45 @@ watch(() => socket.isConnected, async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleClick = item => {
|
||||||
|
if ('video' === item.content_type) {
|
||||||
|
model_item.value = {
|
||||||
|
"type": 'video',
|
||||||
|
"filename": item.path,
|
||||||
|
"folder": "",
|
||||||
|
"extras": {},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['text', 'subtitle', 'metadata'].includes(item.content_type)) {
|
||||||
|
model_item.value = {
|
||||||
|
"type": 'text',
|
||||||
|
"filename": makeDownload(config, { "filename": item.path }),
|
||||||
|
"folder": "",
|
||||||
|
"extras": {},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('image' === item.content_type) {
|
||||||
|
model_item.value = {
|
||||||
|
"type": 'image',
|
||||||
|
"filename": makeDownload(config, { "filename": item.path }),
|
||||||
|
"folder": "",
|
||||||
|
"extras": {},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('dir' === item.content_type) {
|
||||||
|
reloadContent(item.path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location = makeDownload(config, { "filename": item.path, "folder": "", "extras": {} })
|
||||||
|
}
|
||||||
|
|
||||||
const reloadContent = async (dir = '/', fromMounted = false) => {
|
const reloadContent = async (dir = '/', fromMounted = false) => {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
@ -230,4 +302,12 @@ const makeBreadCrumb = path => {
|
||||||
return links
|
return links
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
watch(model_item, v => {
|
||||||
|
if (!bg_enable.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('body').setAttribute("style", `opacity: ${v ? 1 : bg_opacity.value}`)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue