// ==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);
}
}
}