diff --git a/apps/app/src/media-note/playlist/export.ts b/apps/app/src/media-note/playlist/export.ts new file mode 100644 index 00000000..862ccdae --- /dev/null +++ b/apps/app/src/media-note/playlist/export.ts @@ -0,0 +1,69 @@ +import { pathToFileURL } from "url"; +import type { Vault } from "obsidian"; +import { FileSystemAdapter, Notice } from "obsidian"; + +import { isFileMediaInfo } from "@/info/media-info"; +import { MediaURL } from "@/info/media-url"; +import { toInfoKey } from "../note-index/def"; +import type { Playlist } from "./def"; + +export function generateM3U8File(playlist: Playlist, vault: Vault) { + // Start of the M3U8 file + let m3u8Content = "#EXTM3U\n"; + + // Iterate over the playlist items and add them to the file content + const skippedItems: string[] = []; + let fileNotSupported = false; + for (const item of playlist.list) { + if (item.media instanceof MediaURL) { + // Ensure there's a media URL + m3u8Content += `#EXTINF:-1,${item.title}\n${item.media.href}\n`; + } else if (isFileMediaInfo(item.media)) { + if (vault.adapter instanceof FileSystemAdapter) { + const fileFullPath = vault.adapter.getFullPath(item.media.file.path); + try { + const fileUrl = pathToFileURL(fileFullPath).href; + m3u8Content += `#EXTINF:-1,${item.title}\n${fileUrl}\n`; + } catch (e) { + new Notice(`Failed to convert file path to URL: ${e}`); + skippedItems.push(item.title || toInfoKey(item.media)); + } + } else { + fileNotSupported = true; + skippedItems.push(item.title || toInfoKey(item.media)); + } + } + } + if (skippedItems.length > 0) { + if (fileNotSupported) { + new Notice( + createFragment((f) => { + f.createDiv({ + text: `File URI is not supported in this environment. `, + }); + f.createDiv({ text: `Skipped items: ${skippedItems.join(", ")}` }); + }), + ); + } else { + new Notice(`Skipped items: ${skippedItems.join(", ")}`); + } + } + + // Convert the file content to a Blob + saveM3U8(m3u8Content, playlist.title); +} +function saveM3U8(m3u8Content: string, title: string) { + const blob = new Blob([m3u8Content], { + type: "application/vnd.apple.mpegurl", + }); + + // Create a temporary anchor element to trigger the download + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = `${title}.m3u8`; + + // Append the anchor to the body, click it, and then remove it + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); +} diff --git a/apps/app/src/media-note/playlist/index.ts b/apps/app/src/media-note/playlist/index.ts index c5aaa69e..db18aee1 100644 --- a/apps/app/src/media-note/playlist/index.ts +++ b/apps/app/src/media-note/playlist/index.ts @@ -7,6 +7,7 @@ import { iterateFiles } from "../../lib/iterate-files"; import { toInfoKey } from "../note-index/def"; import { emptyLists } from "./def"; import type { PlaylistWithActive, Playlist } from "./def"; +import { generateM3U8File } from "./export"; import { getPlaylistMeta } from "./extract"; export class PlaylistIndex extends Component { @@ -135,5 +136,37 @@ export class PlaylistIndex extends Component { waitUntilResolve(this.app.metadataCache, this).then(() => { this.onResolve(); }); + this.plugin.addCommand({ + id: "playlist-export", + name: "Export current playlist to m3u8 file", + editorCheckCallback: (checking, editor, ctx) => { + if (!ctx.file || !this.listFileCache.has(ctx.file.path)) return false; + if (checking) return true; + generateM3U8File( + this.listFileCache.get(ctx.file.path)!, + this.app.vault, + ); + }, + }); + this.registerEvent( + this.app.workspace.on( + "file-menu", + (menu, file, source) => + source === "more-options" && + this.listFileCache.has(file.path) && + menu.addItem((item) => + item + .setTitle("Export to m3u8...") + .setIcon("file-down") + .setSection("action") + .onClick(() => { + generateM3U8File( + this.listFileCache.get(file.path)!, + this.app.vault, + ); + }), + ), + ), + ); } }