为Alist生成m3u播放列表文件

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

当前为 2024-08-13 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         为Alist生成m3u播放列表文件
// @namespace    createM3UforAlist.whatGUI
// @version      2024-08-04
// @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", async ()=>{
        if(buttonLock === true) return;
        buttonLock = true
        await uploadM3U()
        buttonLock = false
    });
    document.querySelector(".m3u-download").addEventListener("click", async ()=>{
        if(buttonLock === true) return;
        buttonLock = true
        await downloadM3U()
        buttonLock = false
    });
}

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

async function downloadM3U() {
    // 创建一个隐藏的 <a> 标签
    const link = document.createElement("a");
    let files = await getFileList();
    let m3uBlob = generateM3U(files);
    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);
        }
    }
}