Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Support download anime #151

Merged
merged 6 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,9 @@
],
"editor.tabCompletion": "on",
"diffEditor.codeLens": true,
"bun.runtime": "/home/gitpod/.bun/bin/bun"
}
"bun.runtime": "/home/gitpod/.bun/bin/bun",

"css.customData": [
".vscode/tailwind.json"
]
}
55 changes: 55 additions & 0 deletions .vscode/tailwind.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
}
]
},
{
"name": "@responsive",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
}
]
},
{
"name": "@screen",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
}
]
},
{
"name": "@variants",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
}
]
}
]
}
Binary file modified bun.lockb
Binary file not shown.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"release": "bumpp package.json --commit --tag --push"
},
"dependencies": {
"@ffmpeg/core": "^0.12.6",
"@ffmpeg/ffmpeg": "^0.12.10",
"@firebase/analytics": "^0.9.5",
"@firebase/app": "^0.9.15",
"@firebase/firestore": "^3.13.0",
Expand All @@ -42,6 +44,8 @@
"fb-comments-web": "^0.0.13",
"filesize": "^10.1.0",
"group-array": "^1.0.0",
"hls-parser": "^0.13.3",
"hls-to-mp4-browser": "git+https://github.com/anime-vsub/hls-to-mp4-browser.git",
"htmlparser2": "^8.0.2",
"idb-keyval": "^6.2.1",
"iso-639-1": "^2.1.15",
Expand Down
6 changes: 5 additions & 1 deletion quasar.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ export default configure(function (/* ctx */) {

extendViteConf(viteConf) {
extend(true, viteConf, {
optimizeDeps: {
exclude: ["@ffmpeg/ffmpeg"]
},

resolve: {
alias: {
path: "path-browserify"
Expand Down Expand Up @@ -182,7 +186,7 @@ export default configure(function (/* ctx */) {
"vue-router",
{
quasar: ["useQuasar"],
"vue-i18n": ["useI18n"],
"vue-i18n": ["useI18n"]
// "@vueuse/core": ["computedAsync"]
}
],
Expand Down
11 changes: 10 additions & 1 deletion src/i18n/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,5 +309,14 @@
"msg-pre-resolve": "Network request resolution mode (the higher the request request, the more hardware resources are consumed)",
"nong": "Hot",
"tat": "Turn off",
"val-yeu-cau": "{0} request"
"val-yeu-cau": "{0} request",
"cancel-by-user": "Cancel by user",
"chap-not-found": "Chap {0} not found",
"chat-luong-tai-xuong": "Download quality",
"da-tai-xong-video": "The video has been downloaded",
"dlg": "Downloading...",
"msg-close-tab": "You can safely close this window",
"msg-keep-tab": "Keep this window open to continue",
"msg-ques-quality": "The higher the quality, the more memory it takes up and the longer it takes to load",
"tai-video-that-bai": "Video download failed"
}
11 changes: 10 additions & 1 deletion src/i18n/messages/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,5 +303,14 @@
"msg-pre-resolve": "ネットワーク要求解決モード (要求要求が高くなるほど、より多くのハードウェア リソースが消費されます)",
"nong": "熱い",
"tat": "消す",
"val-yeu-cau": "{0} リクエスト"
"val-yeu-cau": "{0} リクエスト",
"cancel-by-user": "ユーザーによるキャンセル",
"chap-not-found": "章 {0} が見つかりません",
"chat-luong-tai-xuong": "ダウンロード品質",
"da-tai-xong-video": "ビデオがダウンロードされました",
"dlg": "ダウンロード中...",
"msg-close-tab": "このウィンドウは安全に閉じても大丈夫です",
"msg-keep-tab": "続行するには、このウィンドウを開いたままにしてください",
"msg-ques-quality": "品質が高くなるほど、必要なメモリが増え、読み込みに時間がかかります",
"tai-video-that-bai": "ビデオのダウンロードに失敗しました"
}
11 changes: 10 additions & 1 deletion src/i18n/messages/vi-VN.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,5 +299,14 @@
"msg-pre-resolve": "Chế độ giải quyết yêu cầu mạng (yêu cầu giải quyết càng cao thì càng tốn tài nguyên phần cứng)",
"tat": "Tắt",
"val-yeu-cau": "{0} yêu cầu",
"nong": "Nóng"
"nong": "Nóng",
"da-tai-xong-video": "Đã tải xong video",
"msg-close-tab": "Bạn có thể yên tâm đóng cửa sổ này",
"tai-video-that-bai": "Tải video thất bại",
"chat-luong-tai-xuong": "Chất lượng tải xuống",
"msg-ques-quality": "Chất lượng càng cao thì chiếm dụng bộ nhớ càng lớn và thời gian tải càng lâu",
"cancel-by-user": "Cancel by user",
"dlg": "Đang tải xuống...",
"msg-keep-tab": "Giữ cửa sổ này mở để tiếp tục",
"chap-not-found": "Chap {0} not found"
}
11 changes: 10 additions & 1 deletion src/i18n/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,5 +303,14 @@
"msg-pre-resolve": "网络请求解析方式(请求请求越高,消耗硬件资源越多)",
"nong": "热的",
"tat": "关",
"val-yeu-cau": "{0}请求"
"val-yeu-cau": "{0}请求",
"cancel-by-user": "由用户取消",
"chap-not-found": "未找到第 {0} 章",
"chat-luong-tai-xuong": "下载质量",
"da-tai-xong-video": "视频已下载",
"dlg": "正在下载...",
"msg-close-tab": "您可以安全地关闭此窗口",
"msg-keep-tab": "保持此窗口打开以继续",
"msg-ques-quality": "质量越高,占用内存越大,加载时间越长",
"tai-video-that-bai": "视频下载失败"
}
96 changes: 96 additions & 0 deletions src/logic/convert-hls-to-mp4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import wasmURL from "@ffmpeg/core/wasm?url"
import coreURL from "@ffmpeg/core?url"
import { FFmpeg } from "@ffmpeg/ffmpeg"
import { parse, stringify } from "hls-parser"
import pLimit from "p-limit"
import sha256 from "sha256"
import type { RetryOptions } from "ts-retry";
import { retryAsync } from "ts-retry"

/**
* Converts an HLS (HTTP Live Streaming) manifest to an MP4 file.
*
* @param {string} m3u8Content - The content of the HLS manifest file.
* @param {(url: string) => Promise<Uint8Array>} fetchFile - A function to fetch a file from a URL.
* @param {(current: number, total: number, timeCurrent: number, timeDuration: number) => void} onProgress - A callback function to report the progress of the conversion.
* @param {number} [concurrency=20] - The number of concurrent downloads.
* @param {RetryOptions} [retryOptions] - Options for retrying failed downloads.
* @return {Promise<FileData>} The MP4 file data.
*/
export async function convertHlsToMP4(
m3u8Content: string,
fetchFile: (url: string) => Promise<Uint8Array>,
onProgress: (
current: number,
total: number,
timeCurrent: number,
timeDuration: number,
) => void,
concurrency = 20,
retryOptions?: RetryOptions,
) {
const ffmpeg = new FFmpeg()

const hash = sha256(m3u8Content)
const manifest = parse(m3u8Content)

if (!("segments" in manifest))
throw new Error("Can't support master playlist")

if (import.meta.env.DEV)
ffmpeg.on("log", ({ message }) => {
console.log(`[@ffmpeg/ffmpeg]: ${message}`)
})

if (import.meta.env.DEV)
ffmpeg.on("progress", ({ progress, time }) => {
console.log(`${progress * 100} % (transcoded time: ${time / 1000000} s)`)
})

await ffmpeg.load({
coreURL,
wasmURL,
})

const limit = pLimit(concurrency)

const timeDuration = manifest.segments.reduce(
(prev, cur) => cur.duration + prev,
0,
)
// Download all the TS segments
let downloaded = 0
let timeCurrent = 0
await Promise.all(
manifest.segments.map((segment, i) =>
limit(() =>
retryAsync<void>(async () => {
const path = `${hash}-${i}.ts`
await ffmpeg.writeFile(path, await fetchFile(segment.uri))
segment.uri = path
downloaded++
timeCurrent += segment.duration
onProgress(
downloaded,
manifest.segments.length,
timeCurrent,
timeDuration,
)
}, retryOptions),
),
),
)
await ffmpeg.writeFile(`${hash}-media.m3u8`, stringify(manifest))

await ffmpeg.exec([
"-i",
`${hash}-media.m3u8`,
..."-acodec copy -vcodec copy".split(" "),
`${hash}-output.mp4`,
])

// Retrieve the output file
const mp4Data = await ffmpeg.readFile(`${hash}-output.mp4`)

return mp4Data
}
79 changes: 79 additions & 0 deletions src/logic/download-to-mp4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { i18n } from "boot/i18n"
import type PhimId from "src/apis/parser/phim/[id]"
import type PhimIdChap from "src/apis/parser/phim/[id]/[chap]"

import Worker from "./download-to-mp4.worker?worker"

export function downloadToMp4(
url: string,
season: Awaited<ReturnType<typeof PhimId>>,
chaps: Awaited<ReturnType<typeof PhimIdChap>>,
currentChapId: string,
onProgress: (
current: number,
total: number,
tCurrent: number,
tDuration: number,
speed: number
) => void
) {
return new Promise<void>((resolve, reject) => {
const worker = new Worker()
worker.onmessage = (
event: MessageEvent<
| {
ok: boolean
buffer: ArrayBuffer
message?: string
}
| [
current: number,
total: number,
tCurrent: number,
tDuration: number,
speed: number
]
>
) => {
if ("ok" in event.data) {
if (event.data.ok) {
if (event.data.buffer) {
// save to buffer
const url = URL.createObjectURL(
new Blob([event.data.buffer], { type: "video/mp4" })
)

const a = document.createElement("a")
a.href = url
a.id = "temp_download"

a.download = `[animevsub.eu.org] ${i18n.global.t(
"tap-_chap-_name-_othername",
[
chaps.chaps.find((item) => item.id === currentChapId)?.name,
season.name,
season.othername
]
)}.mp4`
document.body.appendChild(a)
a.click()

// URL.revokeObjectURL(url)
document.body.removeChild(a)
}

resolve()
worker.terminate()
} else reject(new Error(event.data.message ?? ""))
} else {
onProgress(...event.data)
}
}
worker.onerror = (event) => reject(event)
// worker.onmessageerror = (event) => reject(event)

worker.postMessage({
url
})
})
}
Loading
Loading