├── screenshot.gif ├── run.sh ├── Dockerfile ├── config ├── config.yaml └── .tidal-dl.json ├── docker.env ├── README.md └── bot.js /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcalacci/tidal-discord/HEAD/screenshot.gif -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker run --env-file docker.env -v /media/nfs/music/@library:/music -v /media/nfs/music/import/discord:/downloads dcalacci/tidal-discord:latest 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nikolaik/python-nodejs:python3.7-nodejs15 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | RUN pip install --upgrade pip 6 | RUN pip install tidal-dl beets --upgrade 7 | RUN npm install 8 | # copy tidal-dl config 9 | COPY config/.tidal-dl.json /root/.tidal-dl.json 10 | 11 | # RUN envsubst < config/config.template > config/config.yaml 12 | 13 | ENTRYPOINT ["node"] 14 | CMD ["bot.js"] 15 | -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | directory: $BEETS_LIBRARY_DIR 2 | library: $BEETS_LIBRARY_FILE 3 | 4 | import: 5 | move: yes 6 | write: yes 7 | link: no 8 | hardlink: no 9 | resume: no 10 | copy: no 11 | 12 | replace: 13 | '[\\/]': _ 14 | ^\.: _ 15 | '[\x00-\x1f]': _ 16 | '[<>:"\?\*\|]': _ 17 | \.$: _ 18 | \s+$: '' 19 | ^\s+: '' 20 | ^-: _ 21 | 22 | plugins: fetchart embedart 23 | 24 | match: 25 | strong_rec_thresh: 0.1 26 | medium_rec_thresh: 0.1 27 | rec_gap_thresh: 0.1 28 | -------------------------------------------------------------------------------- /docker.env: -------------------------------------------------------------------------------- 1 | # discord client token for your bot app 2 | DISCORD_CLIENT_TOKEN=mydiscordbottoken 3 | # keep this as defined here - this is the path (in container) to the tidal-dl binary after install 4 | TIDAL_DL_PATH=/usr/local/bin/tidal-dl 5 | # keep this as defined - this is the temporary download directory for tidal 6 | TIDAL_OUTPUT_PATH=/downloads 7 | # path (in container) to your beets config file. 8 | BEETS_CONFIG_FILE=/app/config/config.yaml 9 | # path (in container) to your beets music library 10 | BEETS_LIBRARY_DIR=/music 11 | # path (in container) to your beets music library database file 12 | BEETS_LIBRARY_FILE=/music/library.db 13 | -------------------------------------------------------------------------------- /config/.tidal-dl.json: -------------------------------------------------------------------------------- 1 | { 2 | "addAlbumIDBeforeFolder": false, 3 | "addExplicitTag": true, 4 | "addHyphen": true, 5 | "addYear": false, 6 | "albumFolderFormat": "{ArtistName}/{Flag} {AlbumTitle} [{AlbumID}] [{AlbumYear}]", 7 | "artistBeforeTitle": false, 8 | "audioQuality": "Master", 9 | "checkExist": true, 10 | "downloadPath": "/downloads", 11 | "getAudioQuality": null, 12 | "getDefaultAlbumFolderFormat": null, 13 | "getDefaultTrackFileFormat": null, 14 | "getVideoQuality": null, 15 | "includeEP": false, 16 | "language": "0", 17 | "multiThreadDownload": true, 18 | "onlyM4a": true, 19 | "read": null, 20 | "save": null, 21 | "saveCovers": true, 22 | "showProgress": true, 23 | "trackFileFormat": "{TrackNumber} - {ArtistName} - {TrackTitle}{ExplicitFlag}", 24 | "usePlaylistFolder": true, 25 | "useTrackNumber": true, 26 | "videoQuality": "P360" 27 | } 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tidal-discord 2 | 3 | ![Importing music by sharing a link to a tidal resource](screenshot.gif) 4 | 5 | Discord bot to download music using [tidal-dl](https://github.com/yaronzz/Tidal-Media-Downloader) and import to an existing [beets](https://github.com/beetbox/beets) 6 | library. 7 | 8 | ** this project assumes you have an already-existing beets library** 9 | 10 | To use: 11 | 12 | ```bash 13 | git clone https://github.com/dcalacci/tidal-discord && cd tidal-discord 14 | docker build -t dcalacci/tidal-discord . 15 | docker run --env-file docker.env -v /media/nfs/music/@library:/music -v /media/nfs/music/import/discord:/downloads dcalacci/tidal-discord:latest 16 | ``` 17 | 18 | Volumes, explained: 19 | 20 | - `/music` in the container should point to your current beets music library directory 21 | - `/downloads` should point to wherever you want `tidal-dl` to download files from tidal, before 22 | they're imported into your beets library. 23 | 24 | To run the container, you need to set a few env vars. These are dependent on how you mount your 25 | volumes. For example, if you mount your beets library directory at `/data/music` in the container, 26 | you should set `BEETS_LIBRARY_DIR=/data/music`. 27 | 28 | Save the following as `docker.env`: 29 | 30 | ``` 31 | # discord client token for your bot app 32 | DISCORD_CLIENT_TOKEN=mydiscordbottoken 33 | # keep this as defined here - this is the path (in container) to the tidal-dl binary after install 34 | TIDAL_DL_PATH=/usr/local/bin/tidal-dl 35 | # keep this as defined - this is the temporary download directory for tidal 36 | TIDAL_OUTPUT_PATH=/downloads 37 | # path (in container) to your beets config file. 38 | BEETS_CONFIG_FILE=/app/config/config.yaml 39 | # path (in container) to your beets music library 40 | BEETS_LIBRARY_DIR=/music 41 | # path (in container) to your beets music library database file 42 | BEETS_LIBRARY_FILE=/music/library.db 43 | ``` 44 | 45 | ## Beets and tidal configuration 46 | 47 | Config files for beets and tidal live in the `config` directory. I've included an example beets 48 | config, but you should make sure you replace it with your own. 49 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | const Discord = require("discord.js"); 2 | var spawn = require("child_process").spawn; 3 | const client = new Discord.Client(); 4 | 5 | DISCORD_CLIENT_TOKEN = process.env.DISCORD_CLIENT_TOKEN; 6 | TIDAL_DL_PATH = process.env.TIDAL_DL_PATH; 7 | TIDAL_OUTPUT_PATH = process.env.TIDAL_OUTPUT_PATH; 8 | BEETS_CONFIG_FILE = process.env.BEETS_CONFIG_FILE; 9 | BEETS_LIBRARY_FILE = process.env.BEETS_LIBRARY_FILE; 10 | BEETS_LIBRARY_DIR = process.env.BEETS_LIBRARY_DIR; 11 | 12 | // add config paths to our beets config file 13 | 14 | client.on("ready", () => { 15 | console.log(`Logged in as ${client.user.tag}!`); 16 | }); 17 | 18 | client.on("message", async (msg) => { 19 | if (msg.author.bot) { 20 | return; 21 | } 22 | if (msg.content === "ping") { 23 | msg.reply("pong"); 24 | return; 25 | } 26 | // var myRegexp = /listen.tidal.com\/[a-zA-Z0-9()]{1,6}\/([0-9]*)/g; 27 | var myRegexp = /.*tidal.com\/([a-zA-Z]{1,6})\/([0-9]*)/gm; 28 | console.log(msg.content); 29 | var match = myRegexp.exec(msg.content); 30 | if (!match) { 31 | console.log("match:", match); 32 | msg.reply("Sorry, i can only read Tidal URLs."); 33 | return; 34 | } 35 | if (match.length > 0) { 36 | var tidal_id = match[2]; 37 | var resource_type = match[1]; 38 | msg.reply("Found a Tidal ID: " + tidal_id); 39 | msg.reply("Trying to download " + resource_type + ", please wait..."); 40 | var { successes, failures } = await download_with_progress(tidal_id, msg); 41 | // add_to_beets("/media/nfs/music/import/discord", msg); 42 | } else { 43 | msg.reply("Please send a Tidal URL!"); 44 | } 45 | }); 46 | // like from https://stackoverflow.com/questions/5775088/how-to-execute-an-external-program-from-within-node-js 47 | const download_with_progress = async function (tidal_url, msg) { 48 | console.log("starting downloader..."); 49 | console.log("using:", tidal_url); 50 | var prc = spawn(TIDAL_DL_PATH, [ 51 | "-l", 52 | tidal_url, 53 | "--output", 54 | TIDAL_OUTPUT_PATH, 55 | ]); 56 | prc.stdout.setEncoding("utf8"); 57 | parsed_results = []; 58 | var successes = []; 59 | var failures = []; 60 | var sent_success_msg = false; 61 | prc.stdout.on("data", function (data) { 62 | var str = data.toString(); 63 | console.log(str); 64 | var lines = str.split(/(\r?\n)/g); 65 | // console.log(lines.join("")); 66 | if (lines.join("").includes("login code")) { 67 | msg.reply( 68 | "Login code reset. Go to link.tidal.com and enter in code below:\n" + 69 | lines.join("") 70 | ); 71 | } 72 | if (lines.join("").includes("[ERR]")) { 73 | failures.push(lines); 74 | } else if ( 75 | lines.join("").includes("[SUCCESS]") | lines.join("").includes("|") 76 | ) { 77 | successes.push(lines); 78 | if (!sent_success_msg) { 79 | sent_success_msg = true; 80 | msg.reply("Successfully downloading..."); 81 | } 82 | } 83 | }); 84 | prc.on("exit", function (data) { 85 | if (successes.join("").length > 2000) { 86 | msg.reply("Successfully downloaded!"); 87 | } 88 | // msg.reply("Succeeded:\n" + successes.join("")); 89 | if (failures.length > 0) { 90 | msg.reply("Some tracks had errors, see below:\n" + failures.join("")); 91 | } 92 | if (failures.length < successes.length) { 93 | add_to_beets(TIDAL_OUTPUT_PATH, msg); 94 | } 95 | }); 96 | return { failures, successes }; 97 | }; 98 | 99 | const add_to_beets = function (downloaded_path, msg) { 100 | console.log("starting importer..."); 101 | msg.reply("Importing and renaming files..."); 102 | var prc = spawn("beet", [ 103 | "-c", 104 | BEETS_CONFIG_FILE, 105 | "-l", 106 | BEETS_LIBRARY_FILE, 107 | "-d", 108 | BEETS_LIBRARY_DIR, 109 | "import", 110 | "-i", // don't add things that have already been imported 111 | "-C", //move tracks, don't copy 112 | "-q", // don't ask for input 113 | TIDAL_OUTPUT_PATH, 114 | ]); 115 | 116 | prc.stdout.setEncoding("utf8"); 117 | parsed_results = []; 118 | prc.stdout.on("data", function (data) { 119 | var str = data.toString(); 120 | console.log(str); 121 | var lines = str.split(/(\r?\n)/g); 122 | parsed_results.push(lines.join("")); 123 | }); 124 | prc.on("exit", function (data) { 125 | if (parsed_results.join("").length > 2000) { 126 | msg.reply("Successfully imported!"); 127 | } 128 | 129 | console.log("exited:", data); 130 | msg.reply("Added to library. Stdout:\n" + parsed_results.join("")); 131 | }); 132 | }; 133 | 134 | client.login(DISCORD_CLIENT_TOKEN); 135 | --------------------------------------------------------------------------------