From 4d6165b2180bd3487aff1770029a5d368be87cd5 Mon Sep 17 00:00:00 2001
From: RemixDev <RemixDev64@gmail.com>
Date: Tue, 13 Apr 2021 18:40:34 +0200
Subject: [PATCH] Apply settings to track and generate filename & filepath

---
 deemix/downloader.js          |  55 ++++++--
 deemix/settings.js            | 105 ++++++++++++++
 deemix/types/Album.js         |   4 +-
 deemix/types/Artist.js        |   2 +-
 deemix/types/Track.js         |  85 +++++++++++-
 deemix/utils/pathtemplates.js | 253 ++++++++++++++++++++++++++++++++++
 6 files changed, 485 insertions(+), 19 deletions(-)
 create mode 100644 deemix/settings.js
 create mode 100644 deemix/utils/pathtemplates.js

diff --git a/deemix/downloader.js b/deemix/downloader.js
index 43fb7dc..0f3741a 100644
--- a/deemix/downloader.js
+++ b/deemix/downloader.js
@@ -2,9 +2,22 @@ const { Track, AlbumDoesntExists } = require('./types/Track.js')
 const { streamTrack, generateStreamURL } = require('./decryption.js')
 const { TrackFormats } = require('deezer-js')
 const { USER_AGENT_HEADER } = require('./utils/index.js')
+const { DEFAULTS } = require('./settings.js')
+const { generatePath } = require('./utils/pathtemplates.js')
 const got = require('got')
 const fs = require('fs')
 
+const extensions = {
+    [TrackFormats.FLAC]:    '.flac',
+    [TrackFormats.LOCAL]:   '.mp3',
+    [TrackFormats.MP3_320]: '.mp3',
+    [TrackFormats.MP3_128]: '.mp3',
+    [TrackFormats.DEFAULT]: '.mp3',
+    [TrackFormats.MP4_RA3]: '.mp4',
+    [TrackFormats.MP4_RA2]: '.mp4',
+    [TrackFormats.MP4_RA1]: '.mp4'
+}
+
 async function getPreferredBitrate(track, bitrate, shouldFallback, uuid, listener){
   bitrate = parseInt(bitrate)
   if (track.localTrack) { return TrackFormats.LOCAL }
@@ -12,17 +25,17 @@ async function getPreferredBitrate(track, bitrate, shouldFallback, uuid, listene
   let falledBack = false
 
   const formats_non_360 = {
-    "FLAC": TrackFormats.FLAC,
-    "MP3_320": TrackFormats.MP3_320,
-    "MP3_128": TrackFormats.MP3_128
+    [TrackFormats.FLAC]: "FLAC",
+    [TrackFormats.MP3_320]: "MP3_320",
+    [TrackFormats.MP3_128]: "MP3_128"
   }
   const formats_360 = {
-    "MP4_RA3": TrackFormats.MP4_RA3,
-    "MP4_RA2": TrackFormats.MP4_RA2,
-    "MP4_RA1": TrackFormats.MP4_RA1
+    [TrackFormats.MP4_RA3]: "MP4_RA3",
+    [TrackFormats.MP4_RA2]: "MP4_RA2",
+    [TrackFormats.MP4_RA1]: "MP4_RA1"
   }
 
-  const is360Format = Object.values(formats_360).indexOf(bitrate) != -1
+  const is360Format = Object.keys(formats_360).indexOf(bitrate) != -1
   let formats
   if (!shouldFallback){
     formats = {...formats_360, ...formats_non_360}
@@ -33,8 +46,8 @@ async function getPreferredBitrate(track, bitrate, shouldFallback, uuid, listene
   }
 
   for (let i = 0; i < Object.keys(formats).length; i++){
-    let formatName = Object.keys(formats)[i]
-    let formatNumber = formats[formatName]
+    let formatNumber = Object.keys(formats)[i]
+    let formatName = formats[formatNumber]
 
     if (formatNumber > bitrate) { continue }
     if (Object.keys(track.filesizes).indexOf(`FILESIZE_${formatName}`) != -1){
@@ -88,7 +101,7 @@ class Downloader {
   constructor(dz, downloadObject, settings, listener){
     this.dz = dz
     this.downloadObject = downloadObject
-    this.settings = settings
+    this.settings = settings || DEFAULTS
     this.bitrate = downloadObject.bitrate
     this.listener = listener
 
@@ -165,12 +178,26 @@ class Downloader {
     track.bitrate = selectedFormat
     track.album.bitrate = selectedFormat
 
-    // Generate covers URLs
     // Apply Settings
+    track.applySettings(this.settings)
+
     // Generate filename and filepath from metadata
-    // Remove Subfolders from filename and add it to filepath
+    let {
+      filename,
+      filepath,
+      artistPath,
+      coverPath,
+      extrasPath
+    } = generatePath(track, this.downloadObject, this.settings)
+
     // Make sure the filepath exsists
+    fs.mkdirSync(filepath, { recursive: true })
+    let writepath = `${filepath}/${filename}${extensions[track.bitrate]}`
+
     // Save extrasPath
+    if (extrasPath && !this.extrasPath) this.extrasPath = extrasPath
+
+    // Generate covers URLs
     // Download and cache the coverart
     // Save local album art
     // Save artist art
@@ -181,9 +208,9 @@ class Downloader {
     // Download the track
     console.log("Downloading")
     track.downloadURL = generateStreamURL(track.id, track.MD5, track.mediaVersion, track.bitrate)
-    let stream = fs.createWriteStream('./writepath')
+    let stream = fs.createWriteStream(writepath)
     await streamTrack(stream, track, 0, this.downloadObject, this.listener)
-    console.log("done")
+    console.log(filename)
     // Adding tags
 
   }
diff --git a/deemix/settings.js b/deemix/settings.js
new file mode 100644
index 0000000..d95fd77
--- /dev/null
+++ b/deemix/settings.js
@@ -0,0 +1,105 @@
+const { TrackFormats } = require('deezer-js')
+
+// Should the lib overwrite files?
+const OverwriteOption = {
+  OVERWRITE: 'y', // Yes, overwrite the file
+  DONT_OVERWRITE: 'n', // No, don't overwrite the file
+  DONT_CHECK_EXT: 'e', // No, and don't check for extensions
+  KEEP_BOTH: 'b', // No, and keep both files
+  ONLY_TAGS: 't' // Overwrite only the tags
+}
+
+// What should I do with featured artists?
+const FeaturesOption = {
+  NO_CHANGE: "0", // Do nothing
+  REMOVE_TITLE: "1", // Remove from track title
+  REMOVE_TITLE_ALBUM: "3", // Remove from track title and album title
+  MOVE_TITLE: "2" // Move to track title
+}
+
+const DEFAULTS = {
+  downloadLocation: "",
+  tracknameTemplate: "%artist% - %title%",
+  albumTracknameTemplate: "%tracknumber% - %title%",
+  playlistTracknameTemplate: "%position% - %artist% - %title%",
+  createPlaylistFolder: true,
+  playlistNameTemplate: "%playlist%",
+  createArtistFolder: false,
+  artistNameTemplate: "%artist%",
+  createAlbumFolder: true,
+  albumNameTemplate: "%artist% - %album%",
+  createCDFolder: true,
+  createStructurePlaylist: false,
+  createSingleFolder: false,
+  padTracks: true,
+  paddingSize: "0",
+  illegalCharacterReplacer: "_",
+  queueConcurrency: 3,
+  maxBitrate: String(TrackFormats.MP3_320),
+  fallbackBitrate: true,
+  fallbackSearch: false,
+  logErrors: true,
+  logSearched: false,
+  saveDownloadQueue: false,
+  overwriteFile: OverwriteOption.DONT_OVERWRITE,
+  createM3U8File: false,
+  playlistFilenameTemplate: "playlist",
+  syncedLyrics: false,
+  embeddedArtworkSize: 800,
+  embeddedArtworkPNG: false,
+  localArtworkSize: 1400,
+  localArtworkFormat: "jpg",
+  saveArtwork: true,
+  coverImageTemplate: "cover",
+  saveArtworkArtist: false,
+  artistImageTemplate: "folder",
+  jpegImageQuality: 80,
+  dateFormat: "Y-M-D",
+  albumVariousArtists: true,
+  removeAlbumVersion: false,
+  removeDuplicateArtists: false,
+  tagsLanguage: "",
+  featuredToTitle: FeaturesOption.NO_CHANGE,
+  titleCasing: "nothing",
+  artistCasing: "nothing",
+  executeCommand: "",
+  tags: {
+    title: true,
+    artist: true,
+    album: true,
+    cover: true,
+    trackNumber: true,
+    trackTotal: false,
+    discNumber: true,
+    discTotal: false,
+    albumArtist: true,
+    genre: true,
+    year: true,
+    date: true,
+    explicit: false,
+    isrc: true,
+    length: true,
+    barcode: true,
+    bpm: true,
+    replayGain: false,
+    label: true,
+    lyrics: false,
+    syncedLyrics: false,
+    copyright: false,
+    composer: false,
+    involvedPeople: false,
+    source: false,
+    savePlaylistAsCompilation: false,
+    useNullSeparator: false,
+    saveID3v1: true,
+    multiArtistSeparator: "default",
+    singleAlbumArtist: false,
+    coverDescriptionUTF8: false
+  }
+}
+
+module.exports = {
+  OverwriteOption,
+  FeaturesOption,
+  DEFAULTS
+}
diff --git a/deemix/types/Album.js b/deemix/types/Album.js
index b106d9c..bd291d6 100644
--- a/deemix/types/Album.js
+++ b/deemix/types/Album.js
@@ -14,7 +14,7 @@ class Album {
     this.artist = {"Main": []}
     this.artists = []
     this.mainArtist = null
-    this.date = new Date()
+    this.date = null
     this.dateString = ""
     this.trackTotal = "0"
     this.discTotal = "0"
@@ -84,6 +84,7 @@ class Album {
     this.label = albumAPI.label || this.label
     this.explicit = Boolean(albumAPI.explicit_lyrics || false)
     if (albumAPI.release_date){
+      this.date = new Date()
       this.date.year = albumAPI.release_date.substring(0,4)
       this.date.month = albumAPI.release_date.substring(5,7)
       this.date.day = albumAPI.release_date.substring(8,10)
@@ -126,6 +127,7 @@ class Album {
       this.pic.md5 = albumAPI_gw.ALB_PICTURE
     }
     if (albumAPI_gw.PHYSICAL_RELEASE_DATE){
+      this.date = new Date()
       this.date.year = albumAPI_gw.PHYSICAL_RELEASE_DATE.substring(0,4)
       this.date.month = albumAPI_gw.PHYSICAL_RELEASE_DATE.substring(5,7)
       this.date.day = albumAPI_gw.PHYSICAL_RELEASE_DATE.substring(8,10)
diff --git a/deemix/types/Artist.js b/deemix/types/Artist.js
index e739867..4570189 100644
--- a/deemix/types/Artist.js
+++ b/deemix/types/Artist.js
@@ -10,7 +10,7 @@ class Artist {
     this.save = true
   }
 
-  ifVariousArtist(){
+  isVariousArtists(){
     return this.id == VARIOUS_ARTISTS
   }
 }
diff --git a/deemix/types/Track.js b/deemix/types/Track.js
index 07db178..227bde4 100644
--- a/deemix/types/Track.js
+++ b/deemix/types/Track.js
@@ -5,6 +5,8 @@ const { Playlist } = require('./Playlist.js')
 const { Picture } = require('./Picture.js')
 const { Lyrics } = require('./Lyrics.js')
 const { VARIOUS_ARTISTS } = require('./index.js')
+const { changeCase } = require('../utils/index.js')
+const { FeaturesOption } = require('../settings.js')
 
 const {
   generateReplayGainString,
@@ -41,7 +43,6 @@ class Track {
     this.position = null
     this.searched = false
     this.bitrate = 0
-    this.singleDownload = false
     this.dateString = ""
     this.artistsString = ""
     this.mainArtistsString = ""
@@ -277,8 +278,86 @@ class Track {
   }
 
   applySettings(settings){
-    // TODO: Applay settings
-    settings;
+    // Check if should save the playlist as a compilation
+    if (settings.tags.savePlaylistAsCompilation && this.playlist){
+      this.trackNumber = this.position
+      this.discNumber = "1"
+      this.album.makePlaylistCompilation(this.playlist)
+    } else {
+      if (this.album.date) this.date = this.album.date
+    }
+    this.dateString = this.date.format(settings.dateFormat)
+    this.album.dateString = this.album.date.format(settings.dateFormat)
+    if (this.playlist) this.playlist.dateString = this.playlist.date.format(settings.dateFormat)
+
+    // Check various artist option
+    if (settings.albumVariousArtists && this.album.variousArtists){
+      let artist = this.album.variousArtists
+      let isMainArtist = artist.role === "Main"
+
+      if (this.album.artists.indexOf(artist.name) == -1)
+        this.album.artists.push(artist.name)
+
+      if (isMainArtist || !this.album.artsit.Main.contains(artist.name) && !isMainArtist){
+        if (!this.album.artist[artist.role])
+          this.album.artist[artist.role] = []
+        this.album.artist[artist.role].push(artist.name)
+      }
+    }
+    this.album.mainArtist.save = (!this.album.mainArtist.isVariousArtists() || settings.albumVariousArtists && this.album.mainArtist.isVariousArtists())
+
+    // Check removeDuplicateArtists
+    if (settings.removeDuplicateArtists) this.removeDuplicateArtists()
+
+    // Check if user wants the feat in the title
+    if (settings.featuredToTitle == FeaturesOption.REMOVE_TITLE){
+      this.title = this.getCleanTitle()
+    }else if (settings.featuredToTitle == FeaturesOption.MOVE_TITLE){
+      this.title = this.getFeatTitle()
+    }else if (settings.featuredToTitle == FeaturesOption.REMOVE_TITLE_ALBUM){
+      this.title = this.getCleanTitle()
+      this.album.title = this.album.getCleanTitle()
+    }
+
+    // Remove (Album Version) from tracks that have that
+    if (settings.removeAlbumVersion && this.title.indexOf("Album Version") != -1){
+      this.title = this.title.replace(/ ?\(Album Version\)/g, '').trim()
+    }
+
+    // Change title and artist casing if needed
+    if (settings.titleCasing != "nothing"){
+      this.title = changeCase(this.title, settings.titleCasing)
+    } else if (settings.artistCasing != "nothing"){
+      this.mainArtist.name = changeCase(this.mainArtist.name, settings.artistCasing)
+      this.artists.forEach((artist, i) => {
+        this.artists[i] = changeCase(artist, settings.artistCasing)
+      })
+      Object.keys(this.artist).forEach((art_type) => {
+        this.artist[art_type].forEach((artist, i) => {
+          this.artist[art_type][i] = changeCase(artist, settings.artistCasing)
+        })
+      })
+    }
+
+    // Generate artist tag
+    if (settings.tags.multiArtistSeparator == "default"){
+      if (settings.featuredToTitle == FeaturesOption.MOVE_TITLE){
+        this.artistsString = this.artist.Main.join(", ")
+      } else {
+        this.artistString = this.artists.join(", ")
+      }
+    } else if (settings.tags.multiArtistSeparator == "andFeat"){
+      this.artsitsString = this.mainArtistsString
+      if (this.featArtistsString && settings.featuredToTitle != FeaturesOption.MOVE_TITLE)
+        this.artistsString += ` ${this.featArtistsString}`
+    } else {
+      let separator = settings.tags.multiArtistSeparator
+      if (settings.featuredToTitle == FeaturesOption.MOVE_TITLE){
+        this.artsitsString = this.artsit.Main.join(separator)
+      } else {
+        this.artsitsString = this.artists.join(separator)
+      }
+    }
   }
 }
 
diff --git a/deemix/utils/pathtemplates.js b/deemix/utils/pathtemplates.js
new file mode 100644
index 0000000..0162cdc
--- /dev/null
+++ b/deemix/utils/pathtemplates.js
@@ -0,0 +1,253 @@
+const { TrackFormats } = require('deezer-js')
+
+const bitrateLabels = {
+    [TrackFormats.MP4_RA3]: "360 HQ",
+    [TrackFormats.MP4_RA2]: "360 MQ",
+    [TrackFormats.MP4_RA1]: "360 LQ",
+    [TrackFormats.FLAC]   : "FLAC",
+    [TrackFormats.MP3_320]: "320",
+    [TrackFormats.MP3_128]: "128",
+    [TrackFormats.DEFAULT]: "128",
+    [TrackFormats.LOCAL]  : "MP3"
+}
+
+function fixName(txt, char='_'){
+  txt = txt+""
+  txt = txt.replace(/[\0/\\:*?"<>|]/g, char)
+  return txt.normalize('NFC')
+}
+
+function fixLongName(name){
+  if (name.indexOf('/') != -1){
+    let sepName = name.split('/')
+    name = ""
+    sepName.forEach((txt) => {
+      txt = fixLongName(txt)
+      name += `${txt}/`
+    })
+    name = name.slice(0, -1)
+  } else {
+    name = name.substring(0, 200)
+  }
+  return name
+}
+
+function antiDot(str){
+	while(str[str.length-1] == "." || str[str.length-1] == " " || str[str.length-1] == "\n"){
+		str = str.substring(0,str.length-1);
+	}
+	if(str.length < 1){
+		str = "dot";
+	}
+	return str;
+}
+
+function pad(num, max_val, settings) {
+  let paddingSize;
+  if (parseInt(settings.paddingSize) == 0) {
+    paddingSize = (max_val+"").length
+  } else{
+    paddingSize = ((10 ** (parseInt(settings.paddingSize) - 1))+"").length
+  }
+  if (paddingSize == 1) paddingSize = 2
+
+  if (settings.padTracks) return (num+"").padStart(paddingSize, "0")
+  return (num+"")
+}
+
+function generatePath(track, downloadObject, settings){
+
+  let filenameTemplate = "%artist% - %title%";
+  let singleTrack = false
+  if (downloadObject.type === "track"){
+    if (settings.createSingleFolder) filenameTemplate = settings.albumTracknameTemplate
+    else filenameTemplate = settings.tracknameTemplate
+    singleTrack = true
+  } else if (downloadObject.type === "album") {
+    filenameTemplate = settings.albumTracknameTemplate
+  } else {
+    filenameTemplate = settings.playlistTracknameTemplate
+  }
+
+  let filename = generateTrackName(filenameTemplate, track, settings)
+  let filepath, artistPath, coverPath, extrasPath
+
+  filepath = settings.downloadLocation || "."
+
+  if (settings.createPlaylistFolder && track.playlist && !settings.tags.savePlaylistAsCompilation)
+    filepath += `/${generatePlaylistName(settings.playlistNameTemplate, track.playlist, settings)}`
+
+  if (track.playlist && !settings.tags.savePlaylistAsCompilation)
+    extrasPath = filepath
+
+  if (
+    (settings.createArtistFolder && !track.playlist) ||
+    (settings.createArtistFolder && track.playlist && settings.tags.savePlaylistAsCompilation) ||
+    (settings.createArtistFolder && track.playlist && settings.createStructurePlaylist)
+  ){
+    filepath += `/${generateArtistName(settings.artistNameTemplate, track.album.mainAritst, settings, track.album.rootArtist)}`
+    artistPath = filepath
+  }
+
+
+  if (settings.createAlbumFolder &&
+      (!singleTrack || singleTrack && settings.createSingleFolder) &&
+      (!track.playlist ||
+        (track.playlist && settings.tags.savePlaylistAsCompilation) ||
+        (track.playlist && settings.createStructurePlaylist)
+      )
+  ){
+    filepath += `/${generateAlbumName(settings.albumNameTemplate, track.album, settings, track.playlist)}`
+    coverPath = filepath
+  }
+
+  if (!extrasPath) extrasPath = filepath
+
+  if (
+    parseInt(track.album.discTotal) > 1 && (
+      (settings.createAlbumFolder && settings.createCDFolder) &&
+      (!singleTrack || (singleTrack && settings.createSingleFolder)) &&
+      (!track.playlist ||
+        (track.playlist && settings.tags.savePlaylistAsCompilation)) ||
+        (track.playlist && settings.createStructurePlaylist)
+    )
+  )
+    filepath += `/CD${track.discNumber}`
+
+  // Remove Subfolders from filename and add it to filepath
+  if (filename.indexOf('/') != -1){
+    let tempPath = filename.substring(0, filename.indexOf('/'))
+    filepath += `/${tempPath}`
+    filename = filename.substring(tempPath.length+1)
+  }
+
+  return {
+    filename,
+    filepath,
+    artistPath,
+    coverPath,
+    extrasPath
+  }
+}
+
+function generateTrackName(filename, track, settings){
+  let c = settings.illegalCharacterReplacer
+  filename = filename.replaceAll("%title%", fixName(track.title, c))
+  filename = filename.replaceAll("%artist%", fixName(track.mainArtist.name, c))
+  filename = filename.replaceAll("%artists%", fixName(track.artists.join(", "), c))
+  filename = filename.replaceAll("%allartists%", fixName(track.artistsString, c))
+  filename = filename.replaceAll("%mainartists%", fixName(track.mainArtistsString, c))
+  if (track.featArtistsString) filename = filename.replaceAll("%featartists%", fixName('('+track.featArtistsString+')', c))
+  else filename = filename.replaceAll("%featartists%", '')
+  filename = filename.replaceAll("%album%", fixName(track.album.title, c))
+  filename = filename.replaceAll("%albumartist%", fixName(track.album.mainArtist.name, c))
+  filename = filename.replaceAll("%tracknumber%", pad(track.trackNumber, track.album.trackTotal, settings))
+  filename = filename.replaceAll("%tracktotal%", track.album.trackTotal)
+  filename = filename.replaceAll("%discnumber%", track.discNumber)
+  filename = filename.replaceAll("%disctotal%", track.album.discTotal)
+  if (track.album.genre.length) filename = filename.replaceAll("%genre%", fixName(track.album.genre[0], c))
+  else filename = filename.replaceAll("%genre%", "Unknown")
+  filename = filename.replaceAll("%year%", track.date.year)
+  filename = filename.replaceAll("%date%", track.dateString)
+  filename = filename.replaceAll("%bpm%", track.bpm)
+  filename = filename.replaceAll("%label%", fixName(track.album.label, c))
+  filename = filename.replaceAll("%isrc%", track.ISRC)
+  filename = filename.replaceAll("%upc%", track.album.barcode)
+  filename = filename.replaceAll("%explicit%", track.explicit ? "(Explicit)" : "")
+
+  filename = filename.replaceAll("%track_id%", track.id)
+  filename = filename.replaceAll("%album_id%", track.album.id)
+  filename = filename.replaceAll("%artist_id%", track.mainArtist.id)
+  if (track.playlist){
+    filename = filename.replaceAll("%playlist_id%", track.playlist.playlistID)
+    filename = filename.replaceAll("%position%", pad(track.position, track.playlist.trackTotal, settings))
+  } else {
+    filename = filename.replaceAll("%playlist_id%", '')
+    filename = filename.replaceAll("%position%", pad(track.position, track.album.trackTotal, settings))
+  }
+  filename = filename.replaceAll('\\', '/')
+  return antiDot(fixLongName(filename))
+}
+
+function generateAlbumName(foldername, album, settings, playlist){
+  let c = settings.illegalCharacterReplacer
+  if (playlist && settings.tags.savePlaylistAsCompilation){
+    foldername = foldername.replaceAll("%album_id%", "pl_" + playlist.playlistID)
+    foldername = foldername.replaceAll("%genre%", "Compile")
+  } else {
+    foldername = foldername.replaceAll("%album_id%", album.id)
+    if (album.genre.length) foldername = foldername.replaceAll("%genre%", fixName(album.genre[0], c))
+    else foldername = foldername.replaceAll("%genre%", "Unknown")
+  }
+  foldername = foldername.replaceAll("%album%", fixName(album.title, c))
+  foldername = foldername.replaceAll("%artist%", fixName(album.mainArtist.name, c))
+  foldername = foldername.replaceAll("%artist_id%", album.mainArtist.id)
+  if (album.rootArtist){
+    foldername = foldername.replaceAll("%root_artist%", fixName(album.rootArtist.name, c))
+    foldername = foldername.replaceAll("%root_artist_id%", album.rootArtist.id)
+  } else {
+    foldername = foldername.replaceAll("%root_artist%", fixName(album.mainArtist.name, c))
+    foldername = foldername.replaceAll("%root_artist_id%", album.mainArtist.id)
+  }
+  foldername = foldername.replaceAll("%tracktotal%", album.trackTotal)
+  foldername = foldername.replaceAll("%disctotal%", album.discTotal)
+  foldername = foldername.replaceAll("%type%", fixName(album.recordType.capitalize(), c))
+  foldername = foldername.replaceAll("%upc%", album.barcode)
+  foldername = foldername.replaceAll("%explicit%", album.explicit ? "(Explicit)" : "")
+  foldername = foldername.replaceAll("%label%", fixName(album.label, c))
+  foldername = foldername.replaceAll("%year%", album.date.year)
+  foldername = foldername.replaceAll("%date%", album.dateString)
+  foldername = foldername.replaceAll("%bitrate%", bitrateLabels[parseInt(album.bitrate)])
+
+  foldername = foldername.replaceAll('\\', '/')
+  return antiDot(fixLongName(foldername))
+}
+
+function generateArtistName(foldername, artist, settings, rootArtist){
+  let c = settings['illegalCharacterReplacer']
+  foldername = foldername.replaceAll("%artist%", fixName(artist.name, c))
+  foldername = foldername.replaceAll("%artist_id%", artist.id)
+  if (rootArtist){
+    foldername = foldername.replaceAll("%root_artist%", fixName(rootArtist.name, c))
+    foldername = foldername.replaceAll("%root_artist_id%", rootArtist.id)
+  } else {
+    foldername = foldername.replaceAll("%root_artist%", fixName(artist.name, c))
+    foldername = foldername.replaceAll("%root_artist_id%", artist.id)
+  }
+  foldername = foldername.replaceAll('\\', '/')
+  return antiDot(fixLongName(foldername))
+}
+
+function generatePlaylistName(foldername, playlist, settings){
+  let c = settings['illegalCharacterReplacer']
+  foldername = foldername.replaceAll("%playlist%", fixName(playlist.title, c))
+  foldername = foldername.replaceAll("%playlist_id%", fixName(playlist.playlistID, c))
+  foldername = foldername.replaceAll("%owner%", fixName(playlist.owner['name'], c))
+  foldername = foldername.replaceAll("%owner_id%", playlist.owner['id'])
+  foldername = foldername.replaceAll("%year%", playlist.date.year)
+  foldername = foldername.replaceAll("%date%", playlist.dateString)
+  foldername = foldername.replaceAll("%explicit%", playlist.explicit ? "(Explicit)" : "")
+  foldername = foldername.replaceAll('\\', '/')
+  return antiDot(fixLongName(foldername))
+}
+
+function generateDownloadItemName(foldername, queueItem, settings){
+  let c = settings['illegalCharacterReplacer']
+  foldername = foldername.replaceAll("%title%", fixName(queueItem.title, c))
+  foldername = foldername.replaceAll("%artist%", fixName(queueItem.artist, c))
+  foldername = foldername.replaceAll("%size%", queueItem.size)
+  foldername = foldername.replaceAll("%type%", fixName(queueItem.type, c))
+  foldername = foldername.replaceAll("%id%", fixName(queueItem.id, c))
+  foldername = foldername.replaceAll("%bitrate%", bitrateLabels[parseInt(queueItem.bitrate)])
+  foldername = foldername.replaceAll('\\', '/').replace('/', c)
+  return antiDot(fixLongName(foldername))
+}
+
+module.exports = {
+  generatePath,
+  generateTrackName,
+  generateAlbumName,
+  generateArtistName,
+  generatePlaylistName,
+  generateDownloadItemName
+}