From 316412b78e76da17457d5e641d7300de7d13c502 Mon Sep 17 00:00:00 2001 From: Tachibana Shin Date: Mon, 24 Apr 2023 20:55:08 +0700 Subject: [PATCH] [WIP]: Support other server (#111) * add apis * typo * feat: typo * feat: api * add logic `get-quality-by-label.ts` * typo * [WIP]: support multiple server and multiple quality (1080p, 480p?) * fix test * typo `artQuality` * resolve need change * reactive `artQuality` value invalidate `sources` * remove redundant logic --- src/apis/parser/ajax/player-fb.spec.ts | 15 + src/apis/parser/ajax/player-fb.ts | 13 + src/apis/parser/phim/[id]/[chap].ts | 4 +- src/apis/runs/ajax/player-fb.ts | 23 + src/apis/runs/ajax/player-link.ts | 72 ++++ src/apis/workers/ajax/player-fb.ts | 4 + src/components/BrtPlayer.vue | 569 +++++++++++++++---------- src/components/sources.ts | 21 - src/constants.ts | 4 + src/global.d.ts | 2 +- src/i18n/messages/en-US.json | 3 +- src/i18n/messages/ja-JP.json | 3 +- src/i18n/messages/vi-VN.json | 3 +- src/i18n/messages/zh-CN.json | 3 +- src/logic/get-quality-by-label.spec.ts | 13 + src/logic/get-quality-by-label.ts | 17 + src/logic/unflat.ts | 2 +- src/pages/phim/_season.interface.ts | 26 ++ src/pages/phim/_season.vue | 127 +++--- src/stores/settings.ts | 4 +- 20 files changed, 608 insertions(+), 320 deletions(-) create mode 100644 src/apis/parser/ajax/player-fb.spec.ts create mode 100644 src/apis/parser/ajax/player-fb.ts create mode 100644 src/apis/runs/ajax/player-fb.ts create mode 100644 src/apis/runs/ajax/player-link.ts create mode 100644 src/apis/workers/ajax/player-fb.ts delete mode 100644 src/components/sources.ts create mode 100644 src/logic/get-quality-by-label.spec.ts create mode 100644 src/logic/get-quality-by-label.ts diff --git a/src/apis/parser/ajax/player-fb.spec.ts b/src/apis/parser/ajax/player-fb.spec.ts new file mode 100644 index 00000000..685707f8 --- /dev/null +++ b/src/apis/parser/ajax/player-fb.spec.ts @@ -0,0 +1,15 @@ +import AjaxPlayerFBParser from "./player-fb" + +describe("player", () => { + test("AjaxPlayerFBParser", () => { + expect( + AjaxPlayerFBParser( + '

Chọn Server:

DUFBHDX(ADS)' + ) + ).toEqual({ + id: "2", + play: "api", + hash: "SEAiZc7pSjNkB2hc9Z9HAgcH_OmZzuBHytI_Q51KqtyHg-T0WfhwQAmVV6i4TM4VtHrk09wA6YFztU0wUl4fCw", + }) + }) +}) diff --git a/src/apis/parser/ajax/player-fb.ts b/src/apis/parser/ajax/player-fb.ts new file mode 100644 index 00000000..0ace754d --- /dev/null +++ b/src/apis/parser/ajax/player-fb.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { parserDom } from "../__helpers__/parserDom" + +export default function AjaxPlayerFBParser(html: string) { + const $ = parserDom(html) + const a = $("a:eq(1)") + + return { + id: a.attr("data-id")!, + play: a.attr("data-play")!, + hash: a.attr("data-href")!, + } +} diff --git a/src/apis/parser/phim/[id]/[chap].ts b/src/apis/parser/phim/[id]/[chap].ts index 33acab5e..22e1e66a 100644 --- a/src/apis/parser/phim/[id]/[chap].ts +++ b/src/apis/parser/phim/[id]/[chap].ts @@ -18,7 +18,7 @@ export default function PhimIdChap(html: string) { hash: $item.attr("data-hash")!, name: $item.text().trim(), - } + } as const }) .toArray() const [day, hour, minus] = @@ -32,7 +32,7 @@ export default function PhimIdChap(html: string) { const poster = $(".TPostBg img").attr("src")! return { - chaps, + chaps: chaps as Readonly, update: !day ? null : ([dayTextToNum(day.toLowerCase()), +hour, +minus] as [ diff --git a/src/apis/runs/ajax/player-fb.ts b/src/apis/runs/ajax/player-fb.ts new file mode 100644 index 00000000..966dc5c4 --- /dev/null +++ b/src/apis/runs/ajax/player-fb.ts @@ -0,0 +1,23 @@ +import type AjaxPlayerFBParser from "src/apis/parser/ajax/player-fb" +import Worker from "src/apis/workers/ajax/player-fb?worker" +import { PostWorker } from "src/apis/wrap-worker" +import { post } from "src/logic/http" + +import { PlayerLink } from "./player-link" + +export async function PlayerFB(episodeId: string) { + const { data: json } = await post("/ajax/player?v=2019a", { + episodeId, + backup: 1, + }) + + const data = JSON.parse(json) + + // eslint-disable-next-line functional/no-throw-statement + if (!data.success) throw new Error("Failed load player facebook") + + const config = await PostWorker(Worker, data.html) + + + return await PlayerLink(config) +} diff --git a/src/apis/runs/ajax/player-link.ts b/src/apis/runs/ajax/player-link.ts new file mode 100644 index 00000000..adc5efa1 --- /dev/null +++ b/src/apis/runs/ajax/player-link.ts @@ -0,0 +1,72 @@ +import { getQualityByLabel } from "src/logic/get-quality-by-label" +import { post } from "src/logic/http" + +const addProtocolUrl = (file: string) => + file.startsWith("http") ? file : `https:${file}` + +interface PlayerLinkReturn { + readonly link: { + readonly file: string + readonly label: "FHD|HD" | "HD" | "FHD" | `${720 | 360 | 340}p` + readonly qualityCode: ReturnType + readonly preload?: string + readonly type: + | "hls" + | "aac" + | "f4a" + | "mp4" + | "f4v" + | "m3u" + | "m3u8" + | "m4v" + | "mov" + | "mp3" + | "mpeg" + | "oga" + | "ogg" + | "ogv" + | "vorbis" + | "webm" + | "youtube" + }[] + readonly playTech: "api" | "trailer" +} +export function PlayerLink(config: { + id: string + play: string + hash: string +}): Promise { + const { id, play, hash: link } = config + return post("/ajax/player?v=2019a", { + id, + play, + link, + backuplinks: "1", + }).then(({ data }) => { + // eslint-disable-next-line functional/no-throw-statement + if (!data) throw new Error("unknown_error") + type Writeable = { + -readonly [P in keyof T]: T[P] extends object ? Writeable : T[P] + } + const config = JSON.parse(data) as Writeable + config.link.forEach((item) => { + item.file = addProtocolUrl(item.file) + switch ( + (item.label as typeof item.label | undefined)?.toUpperCase() as + | Uppercase> + | undefined + ) { + case "HD": + if (item.preload) item.label = "FHD|HD" + break + case undefined: + item.label = "HD" + break + } + item.qualityCode = getQualityByLabel(item.label) + item.type ??= "mp4" + }) + + return config + }) +} diff --git a/src/apis/workers/ajax/player-fb.ts b/src/apis/workers/ajax/player-fb.ts new file mode 100644 index 00000000..1587d944 --- /dev/null +++ b/src/apis/workers/ajax/player-fb.ts @@ -0,0 +1,4 @@ +import PlayerFB from "src/apis/parser/ajax/player-fb" +import { WrapWorker } from "src/apis/wrap-worker" + +WrapWorker(PlayerFB) diff --git a/src/components/BrtPlayer.vue b/src/components/BrtPlayer.vue index 07385300..798b19ce 100644 --- a/src/components/BrtPlayer.vue +++ b/src/components/BrtPlayer.vue @@ -581,7 +581,79 @@ class="mr-6 desktop-mode:mr-5 text-weight-normal art-btn" > + {{ settingsStore.player.server }} + + +
+ {{ t('may-chu-phat') }} + + +
+
+ +
    +
  • + {{ label }} +
  • +
+
+
+
+ + + {{ t('may-chu-phat') }} + + + +
  • - {{ html }} + {{ label }}
@@ -739,7 +810,7 @@ anchor="top middle" self="bottom middle" :offset="[-25, 20]" - class="rounded-xl shadow-xl min-w-[200px]" + class="rounded-xl shadow-xl min-w-[200px] flex column flex-nowrap overflow-visible" :class="{ 'm-transparency': settingsStore.ui.menuTransparency, }" @@ -759,9 +830,28 @@ />
+ {{ t('may-chu-phat') }} +
+
+ {{ label }} +
+ +
{{ t("chat-luong") }}
@@ -769,16 +859,15 @@ dense flat no-caps - class="px-2 flex-1 text-weight-norrmal py-2 c--main rounded-xl" - v-for="({ html }, index) in sources" + class="px-2 flex-1 text-weight-norrmal py-2 rounded-xl" + v-for="({ label, qualityCode }) in sources" :class="{ 'c--main': - html === artQuality || - (!artQuality && index === 0), + qualityCode === artQuality, }" - :key="html" - @click="setArtQuality(html)" - >{{ html }}{{ qualityCode }}
@@ -790,7 +879,7 @@ dense flat no-caps - class="px-2 flex-1 text-weight-norrmal py-2 c--main rounded-xl" + class="px-2 flex-1 text-weight-norrmal py-2 rounded-xl" v-for="{ name, value } in playbackRates" :key="name" :class=" @@ -1000,8 +1089,13 @@ import { QTooltip, useQuasar, } from "quasar" +import type { PlayerLink } from "src/apis/runs/ajax/player-link" import { useMemoControl } from "src/composibles/memo-control" -import { DELAY_SAVE_VIEWING_PROGRESS, playbackRates } from "src/constants" +import { + DELAY_SAVE_VIEWING_PROGRESS, + playbackRates, + servers, +} from "src/constants" import { checkContentEditable } from "src/helpers/checkContentEditable" import { scrollXIntoView, scrollYIntoView } from "src/helpers/scrollIntoView" import { fetchJava } from "src/logic/fetchJava" @@ -1028,8 +1122,6 @@ import { import { useI18n } from "vue-i18n" import { onBeforeRouteLeave, useRouter } from "vue-router" -import type { Source } from "./sources" - const { t } = useI18n() // fix toolip fullscreen not hide if change fullscreen @@ -1055,7 +1147,7 @@ interface SiblingChap { } const props = defineProps<{ - sources?: Source[] + sources?: Awaited>["link"] currentSeason: string nameCurrentSeason?: string currentChap?: string @@ -1176,8 +1268,15 @@ const menuChapsRef = ref() // =========================== huuuu player API。馬鹿馬鹿しい ==================================== const currentStream = computed(() => { - return props.sources?.find((item) => item.html === artQuality.value) + return props.sources?.find((item) => item.qualityCode === artQuality.value) }) +if (import.meta.env.DEV) + watch( + () => props.sources, + (sources) => { + console.log("sources changed: ", sources) + } + ) const video = ref() watch( @@ -1335,8 +1434,18 @@ watch( () => tooltipModeMovieRef.value?.hide() ) -const artQuality = ref() -const setArtQuality = (value: string) => { +const _artQuality = ref>["link"][0]['qualityCode']>() +const artQuality = computed({ + get() { + if (props.sources?.find((item) => item.qualityCode === _artQuality.value)) return _artQuality.value + + return props.sources?.[0]?.qualityCode + }, + set(value) { +_artQuality.value = value + } +}) +const setArtQuality = (value: Exclude) => { artQuality.value = value addNotice(t("chat-luong-da-chuyen-sang-_value", [value])) } @@ -1492,6 +1601,7 @@ function onVideoTimeUpdate() { !currentingTime.value && artControlShow.value && !showMenuQuality.value && + !showMenuServer.value && !showMenuPlaybackRate.value && !showMenuSettings.value && !showMenuSelectChap.value && @@ -1649,8 +1759,26 @@ function runRemount() { // eslint-disable-next-line functional/no-let let currentHls: Hls onBeforeUnmount(() => currentHls?.destroy()) -function remount(resetCurrentTime?: boolean) { - currentHls?.destroy() +function remount(resetCurrentTime?: boolean, noDestroy = false) { + if (!noDestroy) currentHls?.destroy() + else { + const type = currentStream.value?.type + + if ( + (type === "hls" || type === "m3u" || type === "m3u8") && + Hls.isSupported() + ) { + // current stream is HLS -> no cancel if canPlay + } else { + console.warn("can't play HLS stream") + // cancel + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + video.value!.oncanplay = function () { + currentHls?.destroy() + this.oncanplay = null + } + } + } if (!currentStream.value) { $q.notify({ @@ -1660,201 +1788,200 @@ function remount(resetCurrentTime?: boolean) { return } - const { url, type } = currentStream.value + const { file, type } = currentStream.value const currentTime = artCurrentTime.value const playing = artPlaying.value || artEnded artEnded = false - switch (type) { - case "hls": - case "m3u": - // eslint-disable-next-line no-case-declarations - const hls = new Hls({ - debug: import.meta.env.isDev, - progressive: true, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pLoader: class CustomLoader extends (Hls.DefaultConfig.loader as any) { - loadInternal(): void { - const { config, context } = this - if (!config) { - return - } - - const { stats } = this - stats.loading.first = 0 - stats.loaded = 0 - - const controller = new AbortController() - const xhr = (this.loader = { - readyState: 0, - status: 0, - abort() { - controller.abort() - }, - onreadystatechange: <(() => void) | null>null, - onprogress: < - ((eventt: { loaded: number; total: number }) => void) | null - >null, - response: null, - responseText: null, - }) - const headers = new Headers() - if (this.context.headers) - for (const [key, val] of Object.entries(this.context.headers)) - headers.set(key, val as string) - - if (context.rangeEnd) { - headers.set( - "Range", - "bytes=" + context.rangeStart + "-" + (context.rangeEnd - 1) - ) - } + if ( + (type === "hls" || type === "m3u" || type === "m3u8") && + Hls.isSupported() + ) { + const hls = new Hls({ + debug: import.meta.env.isDev, + progressive: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pLoader: class CustomLoader extends (Hls.DefaultConfig.loader as any) { + loadInternal(): void { + const { config, context } = this + if (!config) { + return + } - xhr.onreadystatechange = this.readystatechange.bind(this) - xhr.onprogress = this.loadprogress.bind(this) - self.clearTimeout(this.requestTimeout) - this.requestTimeout = self.setTimeout( - this.loadtimeout.bind(this), - config.timeout + const { stats } = this + stats.loading.first = 0 + stats.loaded = 0 + + const controller = new AbortController() + const xhr = (this.loader = { + readyState: 0, + status: 0, + abort() { + controller.abort() + }, + onreadystatechange: <(() => void) | null>null, + onprogress: < + ((eventt: { loaded: number; total: number }) => void) | null + >null, + response: null, + responseText: null, + }) + const headers = new Headers() + if (this.context.headers) + for (const [key, val] of Object.entries(this.context.headers)) + headers.set(key, val as string) + + if (context.rangeEnd) { + headers.set( + "Range", + "bytes=" + context.rangeStart + "-" + (context.rangeEnd - 1) ) + } - fetchJava(context.url + "#animevsub-vsub", { - headers, - signal: controller.signal, - }) - .then(async (res) => { - // eslint-disable-next-line functional/no-let - let byteLength: number - if (context.responseType === "arraybuffer") { - xhr.response = await res.arrayBuffer() - byteLength = xhr.response.byteLength - } else { - xhr.responseText = await res.text() - byteLength = xhr.responseText.length - } - - xhr.readyState = 4 - xhr.status = 200 - - xhr.onprogress?.({ - loaded: byteLength, - total: byteLength, - }) - // eslint-disable-next-line promise/always-return - xhr.onreadystatechange?.() - }) - .catch((e) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.callbacks!.onError( - { code: xhr.status, text: e.message }, - context, - xhr - ) + xhr.onreadystatechange = this.readystatechange.bind(this) + xhr.onprogress = this.loadprogress.bind(this) + self.clearTimeout(this.requestTimeout) + this.requestTimeout = self.setTimeout( + this.loadtimeout.bind(this), + config.timeout + ) + + fetchJava(context.url + "#animevsub-vsub", { + headers, + signal: controller.signal, + }) + .then(async (res) => { + // eslint-disable-next-line functional/no-let + let byteLength: number + if (context.responseType === "arraybuffer") { + xhr.response = await res.arrayBuffer() + byteLength = xhr.response.byteLength + } else { + xhr.responseText = await res.text() + byteLength = xhr.responseText.length + } + + xhr.readyState = 4 + xhr.status = 200 + + xhr.onprogress?.({ + loaded: byteLength, + total: byteLength, }) - } - } as unknown as PlaylistLoaderConstructor, - }) - currentHls = hls - // customLoader(hls.config) - hls.loadSource(url) + // eslint-disable-next-line promise/always-return + xhr.onreadystatechange?.() + }) + .catch((e) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.callbacks!.onError( + { code: xhr.status, text: e.message }, + context, + xhr + ) + }) + } + } as unknown as PlaylistLoaderConstructor, + }) + currentHls = hls + // customLoader(hls.config) + hls.loadSource(file) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + hls.attachMedia(video.value!) + hls.on(Hls.Events.MANIFEST_PARSED, () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - hls.attachMedia(video.value!) - hls.on(Hls.Events.MANIFEST_PARSED, () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (playing) video.value!.play() - }) - // eslint-disable-next-line no-case-declarations, functional/no-let - let needSwapCodec = false - // eslint-disable-next-line no-case-declarations, functional/no-let, no-undef - let timeoutUnneedSwapCodec: NodeJS.Timeout | number | null = null - hls.on(Hls.Events.ERROR, (event, data) => { - if (data.fatal) { - console.warn("Player fatal: ", data) - switch (data.type) { - case Hls.ErrorTypes.NETWORK_ERROR: { - // try to recover network error - $q.notify({ - message: t("loi-mang-khong-kha-dung"), - position: "bottom-right", - timeout: 0, - actions: [ - { - label: t("thu-lai"), - color: "yellow", - noCaps: true, - handler: () => hls.startLoad(), - }, - { - icon: "close", - round: true, - }, - ], - }) - break + if (playing) video.value!.play() + }) + // eslint-disable-next-line functional/no-let + let needSwapCodec = false + // eslint-disable-next-line functional/no-let, no-undef + let timeoutUnneedSwapCodec: NodeJS.Timeout | number | null = null + hls.on(Hls.Events.ERROR, (event, data) => { + if (data.fatal) { + console.warn("Player fatal: ", data) + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: { + // try to recover network error + $q.notify({ + message: t("loi-mang-khong-kha-dung"), + position: "bottom-right", + timeout: 0, + actions: [ + { + label: t("thu-lai"), + color: "yellow", + noCaps: true, + handler: () => hls.startLoad(), + }, + { + icon: "close", + round: true, + }, + ], + }) + break + } + case Hls.ErrorTypes.MEDIA_ERROR: { + const playing = artPlaying.value + if (timeoutUnneedSwapCodec) { + clearTimeout(timeoutUnneedSwapCodec) + timeoutUnneedSwapCodec = null } - case Hls.ErrorTypes.MEDIA_ERROR: { - const playing = artPlaying.value + console.warn("fatal media error encountered, try to recover") + if (needSwapCodec) { + hls.swapAudioCodec() + needSwapCodec = false if (timeoutUnneedSwapCodec) { clearTimeout(timeoutUnneedSwapCodec) timeoutUnneedSwapCodec = null } - console.warn("fatal media error encountered, try to recover") - if (needSwapCodec) { - hls.swapAudioCodec() + } else { + needSwapCodec = true + timeoutUnneedSwapCodec = setTimeout(() => { needSwapCodec = false - if (timeoutUnneedSwapCodec) { - clearTimeout(timeoutUnneedSwapCodec) - timeoutUnneedSwapCodec = null - } - } else { - needSwapCodec = true - timeoutUnneedSwapCodec = setTimeout(() => { - needSwapCodec = false - timeoutUnneedSwapCodec = null - }, 1_000) - } - hls.recoverMediaError() - if (playing) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - video.value!.play() - break + timeoutUnneedSwapCodec = null + }, 1_000) } - default: { - $q.notify({ - message: t("da-gap-su-co-khi-phat-lai"), - position: "bottom-right", - timeout: 0, - actions: [ - { - label: t("thu-lai"), - color: "white", - handler() { - console.log("retry force") - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - video.value!.load() - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - video.value!.play() - }, - }, - { - label: t("remount"), - color: "white", - handler: remount, + hls.recoverMediaError() + if (playing) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + video.value!.play() + break + } + default: { + $q.notify({ + message: t("da-gap-su-co-khi-phat-lai"), + position: "bottom-right", + timeout: 0, + actions: [ + { + label: t("thu-lai"), + color: "white", + handler() { + console.log("retry force") + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + video.value!.load() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + video.value!.play() }, - ], - }) - break - } + }, + { + label: t("remount"), + color: "white", + handler: remount, + }, + ], + }) + break } - } else { - console.warn("Player error: ", data) } - }) - break - default: - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - video.value!.src = url + } else { + console.warn("Player error: ", data) + } + }) + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + video.value!.src = file } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -1876,40 +2003,33 @@ const watcherVideoTagReady = watch(video, (video) => { // eslint-disable-next-line promise/catch-or-return Promise.resolve().then(watcherVideoTagReady) // fix this not ready value + // eslint-disable-next-line functional/no-let + let currentEpStream: null | string = null watch( - () => currentStream.value?.url, + () => currentStream.value?.file, (url) => { if (!url) return currentHls?.destroy() console.log("set url art %s", url) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((Hls as unknown as any).isSupported()) { - remount(true) - } else { - const canPlay = video.canPlayType("application/vnd.apple.mpegurl") - if (canPlay === "probably" || canPlay === "maybe") { - video.src = url - } - } + // // eslint-disable-next-line @typescript-eslint/no-explicit-any + // if ((Hls as unknown as any).isSupported()) { + remount( + currentEpStream !== uidChap.value, + currentEpStream === uidChap.value + ) + currentEpStream = uidChap.value + // } else { + // const canPlay = video.canPlayType("application/vnd.apple.mpegurl") + // if (canPlay === "probably" || canPlay === "maybe") { + // video.src = url + // } + // } }, { immediate: true } ) }) -// re-set quality if quality not in sources -watch( - () => props.sources, - (sources) => { - if (!sources || sources.length === 0) return - // not ready quality on this - if (!artQuality.value || !currentStream.value) { - artQuality.value = sources[0].html // not use setArtQuality because skip notify - } - }, - { immediate: true } -) - const currentingTime = ref(false) const progressInnerRef = ref() @@ -2167,6 +2287,7 @@ watch(showDialogChapter, (status) => { }) const showMenuQuality = ref(false) +const showMenuServer = ref(false) const showMenuPlaybackRate = ref(false) const showMenuSettings = ref(false) const showMenuSelectChap = ref(false) diff --git a/src/components/sources.ts b/src/components/sources.ts deleted file mode 100644 index cf823341..00000000 --- a/src/components/sources.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface Source { - html: string - url: string - type: - | "hls" - | "aac" - | "f4a" - | "mp4" - | "f4v" - | "m3u" - | "m4v" - | "mov" - | "mp3" - | "mpeg" - | "oga" - | "ogg" - | "ogv" - | "vorbis" - | "webm" - | "youtube" -} diff --git a/src/constants.ts b/src/constants.ts index bde54de5..e6c84211 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,6 +2,10 @@ export const labelToQuality: Record = { HD: "720p", SD: "480p", } +export const servers = { + DU: "High", + FB: "Low", +} as const export const playbackRates = [ { name: "0.5x", diff --git a/src/global.d.ts b/src/global.d.ts index 8b54b48e..3614dfc0 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -12,7 +12,7 @@ interface PostOptions { interface HttpResponse { headers: Record - data: ResponseType extends "arraybuffer" ? ArrayBuffer : string + data: string url: string status: number } diff --git a/src/i18n/messages/en-US.json b/src/i18n/messages/en-US.json index e61f4511..77ec6b32 100644 --- a/src/i18n/messages/en-US.json +++ b/src/i18n/messages/en-US.json @@ -245,5 +245,6 @@ "xoa-danh-sach-phat": "delete playlist", "xoa-khoi-danh-sach-phat": "Remove from playlist", "yeu-cau-dang-nhap-lai": "Requires re-login", - "yeu-thich": "Favourite" + "yeu-thich": "Favourite", + "may-chu-phat": "Broadcast server" } diff --git a/src/i18n/messages/ja-JP.json b/src/i18n/messages/ja-JP.json index a9efe259..cadf60f4 100644 --- a/src/i18n/messages/ja-JP.json +++ b/src/i18n/messages/ja-JP.json @@ -245,5 +245,6 @@ "xoa-danh-sach-phat": "プレイリストを削除", "xoa-khoi-danh-sach-phat": "プレイリストから削除", "yeu-cau-dang-nhap-lai": "再ログインが必要", - "yeu-thich": "お気に入り" + "yeu-thich": "お気に入り", + "may-chu-phat": "ブロードキャスト サーバー" } diff --git a/src/i18n/messages/vi-VN.json b/src/i18n/messages/vi-VN.json index 1d9b06e2..a35269c7 100644 --- a/src/i18n/messages/vi-VN.json +++ b/src/i18n/messages/vi-VN.json @@ -245,5 +245,6 @@ "xoa-danh-sach-phat": "Xóa danh sách phát", "xoa-khoi-danh-sach-phat": "Xóa khỏi danh sách phát", "yeu-cau-dang-nhap-lai": "Yêu cầu đăng nhập lại", - "yeu-thich": "Yêu thích" + "yeu-thich": "Yêu thích", + "may-chu-phat": "Máy chủ phát" } diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 59d2b4d3..5c040b71 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -245,5 +245,6 @@ "xoa-danh-sach-phat": "删除播放列表", "xoa-khoi-danh-sach-phat": "从播放列表中删除", "yeu-cau-dang-nhap-lai": "需要重新登录", - "yeu-thich": "最喜欢的" + "yeu-thich": "最喜欢的", + "may-chu-phat": "广播服务器" } diff --git a/src/logic/get-quality-by-label.spec.ts b/src/logic/get-quality-by-label.spec.ts new file mode 100644 index 00000000..38b0e0f5 --- /dev/null +++ b/src/logic/get-quality-by-label.spec.ts @@ -0,0 +1,13 @@ +import { getQualityByLabel } from "./get-quality-by-label" + +describe("get-quality-by-label", () => { + test("should label is HD", () => { + expect(getQualityByLabel("HD")).toBe("720p") + }) + test("should label is 720p", () => { + expect(getQualityByLabel("720p")).toBe("720p") + }) + test("should label is FHD|HD", () => { + expect(getQualityByLabel("FHD|HD")).toBe("1080p|720p") + }) +}) diff --git a/src/logic/get-quality-by-label.ts b/src/logic/get-quality-by-label.ts new file mode 100644 index 00000000..f812c4f9 --- /dev/null +++ b/src/logic/get-quality-by-label.ts @@ -0,0 +1,17 @@ +import type { PlayerLink } from "src/apis/runs/ajax/player-link" + +const map = { + "FHD|HD": "1080p|720p", + FHD: "1080p", + HD: "720p", + SD: "480p", +} as const +export function getQualityByLabel( + label: Awaited>["link"][0]["label"] +) { + return ( + (map[label as keyof typeof map] as + | (typeof map)[keyof typeof map] + | undefined) ?? (label as Exclude) + ) +} diff --git a/src/logic/unflat.ts b/src/logic/unflat.ts index b777bb73..3ef93759 100644 --- a/src/logic/unflat.ts +++ b/src/logic/unflat.ts @@ -1,4 +1,4 @@ -export function unflat(array: T[], size: number): T[][] { +export function unflat(array: readonly T[], size: number): T[][] { const { length } = array const max = ~~(length / size) diff --git a/src/pages/phim/_season.interface.ts b/src/pages/phim/_season.interface.ts index fa1d3061..23c2f802 100644 --- a/src/pages/phim/_season.interface.ts +++ b/src/pages/phim/_season.interface.ts @@ -26,3 +26,29 @@ export type ProgressWatchStore = Map< status: "queue" } > + +export interface ConfigPlayer { + readonly link: { + readonly file: string + readonly label?: "HD" | "FHD" | `${720 | 360 | 340}p` + readonly preload?: string + readonly type: + | "hls" + | "aac" + | "f4a" + | "mp4" + | "f4v" + | "m3u" + | "m4v" + | "mov" + | "mp3" + | "mpeg" + | "oga" + | "ogg" + | "ogv" + | "vorbis" + | "webm" + | "youtube" + }[] + readonly playTech: "api" | "trailer" +} diff --git a/src/pages/phim/_season.vue b/src/pages/phim/_season.vue index 85df75e1..249a1e25 100644 --- a/src/pages/phim/_season.vue +++ b/src/pages/phim/_season.vue @@ -48,7 +48,7 @@ >
@@ -431,20 +431,18 @@ import { useQuasar, } from "quasar" import { AjaxLike, checkIsLike } from "src/apis/runs/ajax/like" +import { PlayerFB } from "src/apis/runs/ajax/player-fb" +import { PlayerLink } from "src/apis/runs/ajax/player-link" import { AjaxRate } from "src/apis/runs/ajax/rate" import { PhimId } from "src/apis/runs/phim/[id]" import { PhimIdChap } from "src/apis/runs/phim/[id]/[chap]" // import BottomSheet from "src/components/BottomSheet.vue" -import type { Source } from "src/components/sources" -import { - C_URL, - labelToQuality, - TIMEOUT_GET_LAST_EP_VIEWING_IN_STORE, -} from "src/constants" +import type { servers } from "src/constants"; +import { C_URL, TIMEOUT_GET_LAST_EP_VIEWING_IN_STORE } from "src/constants" import { forceHttp2 } from "src/logic/forceHttp2" import { formatView } from "src/logic/formatView" +import { getQualityByLabel } from "src/logic/get-quality-by-label" import { getRealSeasonId } from "src/logic/getRealSeasonId" -import { post } from "src/logic/http" import { parseChapName } from "src/logic/parseChapName" import { unflat } from "src/logic/unflat" import { useAuthStore } from "stores/auth" @@ -885,7 +883,6 @@ watchEffect(() => { const metaEp = epId ? chaps.find((item) => item.id === epId) : chaps[0] if (!metaEp) return - const correctChapName = parseChapName(metaEp.name) const urlChapName = route.params.chapName @@ -1040,28 +1037,19 @@ const prevChap = computed((): SiblingChap | undefined => { console.info("[[===THE END===]]") }) -const configPlayer = shallowRef<{ - link: { - file: string - label: string - preload: string - type: "hls" | "youtube" - }[] - playTech: "api" | "trailer" -}>() +const configPlayer = shallowRef>>() watch( currentMetaChap, - async (currentMetaChap) => { + (currentMetaChap, _, onCleanup) => { if (!currentMetaChap) return - configPlayer.value = undefined - if (currentMetaChap.id === "0") { configPlayer.value = { link: [ { file: currentMetaChap.hash, label: "HD", + qualityCode: getQualityByLabel("HD"), preload: "auto", type: "youtube", }, @@ -1072,56 +1060,63 @@ watch( return } - try { - configPlayer.value = JSON.parse( - ( - await post("/ajax/player?v=2019a", { - link: currentMetaChap.hash, - play: currentMetaChap.play, - id: currentMetaChap.id, - backuplinks: "1", + configPlayer.value = undefined + + // eslint-disable-next-line functional/no-let + let typeCurrentConfig: keyof typeof servers | null = null + // setup watcher it + const watcher = watch( + () => settingsStore.player.server, + async (server) => { + try { + if (server === "DU") { + if (typeCurrentConfig !== "DU") + // eslint-disable-next-line promise/catch-or-return + PlayerLink(currentMetaChap).then((conf) => { + // eslint-disable-next-line promise/always-return + if (settingsStore.player.server === "DU") { + configPlayer.value = conf + typeCurrentConfig = "DU" + } + }) + } + if (server === "FB") { + // PlayerFB は常に PlayerLink よりも遅いため、DU を使用して高速プリロード戦術を使用する必要があります。 + if (typeCurrentConfig !== "DU") + // eslint-disable-next-line promise/catch-or-return + PlayerLink(currentMetaChap).then((conf) => { + // eslint-disable-next-line promise/always-return + if (settingsStore.player.server === "DU") { + configPlayer.value = conf + typeCurrentConfig = "DU" + } + }) + // eslint-disable-next-line promise/catch-or-return + PlayerFB(currentMetaChap.id).then((conf) => { + // eslint-disable-next-line promise/always-return + if (settingsStore.player.server === "FB") { + configPlayer.value = conf + typeCurrentConfig = "FB" + } + }) + } + } catch (err) { + $q.notify({ + position: "bottom-right", + message: (err as Error).message, }) - ).data as string - ) - } catch (err) { - $q.notify({ - position: "bottom-right", - message: (err as Error).message, - }) - console.log({ - err, - }) - } + console.error(err) + } + }, + { immediate: true } + ) + onCleanup(watcher) }, { immediate: true, } ) -const sources = computed(() => - configPlayer.value?.link.map((item): Source => { - return { - html: labelToQuality[item.label] ?? item.label, - url: item.file.startsWith("http") ? item.file : `https:${item.file}`, - type: item.type as - | "aac" - | "f4a" - | "mp4" - | "f4v" - | "hls" - | "m3u" - | "m4v" - | "mov" - | "mp3" - | "mpeg" - | "oga" - | "ogg" - | "ogv" - | "vorbis" - | "webm" - | "youtube", - } - }) -) +const sources = computed(() => configPlayer.value?.link) async function getProgressChaps( currentSeason: string diff --git a/src/stores/settings.ts b/src/stores/settings.ts index b26acf81..12b80dcd 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -1,4 +1,5 @@ import { defineStore } from "pinia" +import type { servers } from "src/constants" import { getNavigatorLanguage } from "src/i18n" export const useSettingsStore = defineStore("settings", { @@ -7,11 +8,12 @@ export const useSettingsStore = defineStore("settings", { autoNext: true, enableRemindStop: true, volume: 1, + server: "DU" }, ui: { modeMovie: false, newPlayer: true, - shortcutsQAP: false, + shortcutsQAP: true, menuTransparency: true, commentAnime: true, },