为Alist生成m3u播放列表文件

为alist中的音视频文件生成并上传或下载一个m3u播放列表文件,脚本编写过程由肉人辅助AI完成

目前為 2024-08-23 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         为Alist生成m3u播放列表文件
// @namespace    createM3UforAlist.whatGUI
// @version      2024-08-23
// @description  为alist中的音视频文件生成并上传或下载一个m3u播放列表文件,脚本编写过程由肉人辅助AI完成
// @author       whatGUI
// @match        http://*/*
// @match        https://*/*
// @grant        GM_setClipboard
// @icon         https://alist.nn.ci/favicon.ico
// @license      MIT

// ==/UserScript==

(function () {
    "use strict";
    addButton();
})();

function addButton() {
    let buttonLock = false;
    const buttonDiv = document.createElement("div");
    const style = document.createElement("style");
    // 设置 CSS 规则
    style.textContent = `
.toolbar-ex {
  position: fixed;
  right: 65px;
  bottom: 20px;
}

.toolbar-ex-icon {
  width: 2rem;
  height: 2rem;
  color: #ff8718;
  padding: 4px;
  border-radius: 0.5rem;
  cursor: pointer;
  margin-top: 0.25rem;
}
.toolbar-ex-icon:hover {
  color: #ffffff;
  background-color: #ff8718;
}
.m3u-method-menu {
  position: absolute;
  bottom: calc( 32px + 0.25rem);
  right: 0;
  transition: height 0.2s ease-out;
  height: 0;
  overflow: hidden;
}
`;
    // 将 <style> 元素插入到 <head> 中
    document.head.appendChild(style);
    buttonDiv.className = "toolbar-ex";
    buttonDiv.innerHTML = `
    <div class="m3u-method-menu hidden">
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="toolbar-ex-icon m3u-upload" viewBox="0 0 16 16">
            <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
            <path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="toolbar-ex-icon m3u-download" viewBox="0 0 16 16">
            <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
            <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/>
        </svg>
        </div>
        <svg fill="none" stroke-width="0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="toolbar-ex-icon toolbar-ex-toggle" height="1em" width="1em" style="overflow: visible;"><path fill="currentColor" d="M7 14a2 2 0 100-4 2 2 0 000 4zM14 12a2 2 0 11-4 0 2 2 0 014 0zM17 14a2 2 0 100-4 2 2 0 000 4z"></path><path fill="currentColor" fill-rule="evenodd" d="M24 12c0 6.627-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0s12 5.373 12 12zm-2 0c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10z" clip-rule="evenodd"></path></svg>
    `;

    document.body.appendChild(buttonDiv);

    document
        .querySelector(".toolbar-ex-toggle")
        .addEventListener("click", function () {
        var menu = document.querySelector(".m3u-method-menu");
        if (menu.classList.contains("hidden")) {
            menu.classList.remove("hidden");
            menu.style.height = "4.5rem";
        } else {
            menu.style.height = "0";
            menu.classList.add("hidden");
        }
    });

    document.querySelector(".m3u-upload").addEventListener("click", uploadM3U);
    document.querySelector(".m3u-download").addEventListener("click", downloadM3U);
}

async function uploadM3U() {
    let files = await getFileList();
    let m3uBlob = generateM3U(files);
    if(!m3uBlob) return;
    await uploadBlob(m3uBlob.blob);
    let m3uURL = await getPlaylistURL();
    //添加地址到剪贴板
    GM_setClipboard(m3uURL);
    alert("m3u上传成功并已复制m3u文件链接到剪贴板,若需查看文件请刷新");
}

async function downloadM3U() {
    let files = await getFileList();
    let m3uBlob = generateM3U(files);
    if(!m3uBlob) return;
    // 创建一个隐藏的 <a> 标签
    const link = document.createElement("a");
    link.href = m3uBlob.href;
    link.download = "playlist.m3u";
    link.style.display = "none";
    document.body.appendChild(link);
    // 触发点击事件来下载文件
    link.click();
    // 清除元素
    document.body.removeChild(link);
}

function isMediaFile(filename) {
    // 定义常见的影音文件扩展名
    const mediaExtensions = [
        ".mp4",
        ".mkv",
        ".mov",
        ".avi",
        ".flv",
        ".wmv",
        ".webm",
        ".wav",
        ".ogg",
        ".mp3",
        ".flac",
        ".aac",
        ".m4a",
        ".ape",
    ];
    // 获取文件扩展名
    const extension = filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2);
    // 检查扩展名是否在常见的影音类型列表中
    return mediaExtensions.includes("." + extension.toLowerCase());
}

function generateM3U(files) {
    if (!files) {
        alert("m3u生成失败:当前页面没有文件");
        return;
    }
    let m3uContent = "#EXTM3U\n";

    let videoCount = 0;
    files.forEach((video) => {
        if (isMediaFile(video.name)) {
            videoCount++;
            m3uContent += `#EXTINF:-1,${video.name}\n${video.url}\n`;
        }
    });

    if (videoCount === 0) {
        alert("m3u生成失败:当前页面没有音视频文件");
        return;
    }

    // 创建一个新的 Blob 对象,将 M3U 内容包装起来
    const blob = new Blob([m3uContent], { type: "application/x-mpegURL" });
    // 创建一个下载链接
    const href = URL.createObjectURL(blob);
    return { blob, href };
}

async function getFileList() {
    const alistListAPI = "/api/fs/list";
    const folderPath = window.location.pathname;
    const decodedPath = decodeURIComponent(folderPath);

    const alistToken = localStorage.getItem("token");

    const headers = new Headers({
        Authorization: alistToken,
        "Content-Type": "application/json",
    });

    const body = JSON.stringify({
        path: decodedPath,
        password: "",
        page: 1,
        per_page: 0,
        refresh: false,
    });

    const requestOptions = {
        method: "POST",
        headers,
        body,
        redirect: "follow",
    };
    let result;
    try {
        const response = await fetch(alistListAPI, requestOptions);
        result = await response.json();
    } catch (error) {
        console.log("error: ", error);
    }

    let fileList = [];
    result.data?.content.forEach((file) => {
        if (!file.is_dir) {
            fileList.push({
                name: file.name,
                url:
                window.location.origin +
                "/d" +
                decodedPath +
                "/" +
                file.name +
                "?sign=" +
                file.sign,
            });
        }
    });
    console.log(fileList);
    return fileList;
}

async function uploadBlob(blob) {
    const alistUploadAPI = "/api/fs/put";
    const alistToken = localStorage.getItem("token");
    const currentURL = decodeURIComponent(window.location.pathname);
    const path = encodeURIComponent(currentURL + "/playlist.m3u");

    // 设置请求头
    const headers = new Headers({
        Authorization: alistToken,
        "File-Path": path, // 注意路径需要 URL 编码
        "Content-Type": "application/x-mpegURL", // M3U 文件的 Content-Type
        "Content-Length": blob.size.toString(),
        As_Task: "false", // 可选,是否作为任务
    });

    // 创建请求体
    const body = blob;

    try {
        const response = await fetch(alistUploadAPI, {
            method: "PUT",
            headers,
            body,
        });

        if (!response.ok) {
            throw new Error(`Failed to upload: ${response.statusText}`);
        }

        const result = await response.json();
        console.log("Upload successful:", result);
    } catch (error) {
        console.error("Error uploading file:", error);
    }
    return window.location.origin + "/" + path;
}

async function getPlaylistURL() {
    const list = await getFileList();
    for (let index = 0; index < list.length; index++) {
        const file = list[index];
        if (file.name === "playlist.m3u") {
            return encodeURI(file.url);
        }
    }
}