Skip to content

Commit

Permalink
WIP: Support download anime (#151)
Browse files Browse the repository at this point in the history
* Add Tailwind CSS custom data and simplify VSCode settings

- Added `.vscode/tailwind.json` with Tailwind CSS directives and descriptions.
- Simplified `.vscode/settings.json` by removing unnecessary configurations and retaining essential settings.

* Add offline video download and management functionality

- Implement video download to MP4 using HLS streams
- Add IDB-keyval for storing video metadata and offline registry
- Create UI components for downloading and removing videos
- Integrate progress tracking and notifications for download status
- Add support for selecting video quality before download
- Implement functions to manage offline video data and storage
- Update package.json with necessary dependencies for video processing
- Optimize Vite configuration for FFmpeg dependencies
- Add constants for offline video storage prefixes and registry
- Implement utility functions for handling before unload events and listing offline seasons

* Add offline mode support with file saving options

- Introduced `questionSaveToFile` function to prompt user for file saving location.
- Enhanced `download` function to handle saving videos to device or app storage.
- Added `getURL` and `getFile` functions for retrieving stored files.
- Updated `SeasonOffline` and `ChapsOffline` interfaces to include offline metadata.
- Implemented retry logic for fetching poster and image buffers.
- Modified `download-to-mp4` worker to support saving video buffers directly to device.
- Created `get-vdm-store` utility for caching VDM store instance.

* Refactor offline video download logic and improve localization

- Removed redundant offline video management functions and constants.
- Simplified the `download-to-mp4` worker by eliminating unnecessary parameters.
- Updated `vdm.ts` store to streamline download process and remove unused functions.
- Enhanced localization support for download-related messages.
- Adjusted UI components to reflect changes in download logic.
- Updated VSCode settings for i18n-ally integration.

* Update VSCode settings for improved development experience

- Enable bracket pair colorization and guides
- Set Prettier as the default formatter and enable format on save
- Configure ESLint validation for multiple languages
- Add custom spell check words and enable Ionic preview in editor
- Update i18n-ally settings for locale paths and extraction rules
- Ignore specific i18n keys in various Vue components
- Add TypeScript SDK path and SCSS linting rule
- Enable tab completion and code lens in diff editor
- Set Bun runtime path for Gitpod environment

* remove loss file
  • Loading branch information
tachibana-shin authored Aug 23, 2024
1 parent a11188f commit e78ff97
Show file tree
Hide file tree
Showing 16 changed files with 654 additions and 7 deletions.
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

0 comments on commit e78ff97

Please sign in to comment.