├── .gitignore ├── LICENSE ├── LICENSE.rtf ├── README.md ├── build.sbt ├── images ├── 00_export_collection.png ├── 01_missing_files.png ├── 02_missing_files_in_playlist.png ├── 03_missing_file_manager.png ├── 04_rekordbox_xml_prefs.png ├── 05_rekordbox_xml_tracks.png ├── 06_rekordbox_xml_playlist.png ├── 07_repaired_collection.png └── 08_repaired_playlist.png ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ ├── logback.xml │ └── report-template.txt └── scala │ └── com │ └── vividlab │ └── rekordbox │ ├── Config.scala │ ├── FileUtils.scala │ ├── Main.scala │ ├── OS.scala │ ├── analyse │ ├── Analyser.scala │ ├── AnalyserResult.scala │ └── LocateFileResult.scala │ ├── data │ ├── CollectionTrack.scala │ ├── Playlist.scala │ └── PlaylistTrack.scala │ └── repair │ ├── RekordBoxRewriteRule.scala │ └── Repairer.scala └── test ├── resources ├── report-templates │ ├── expected-report-macos.txt │ └── expected-report-windows.txt ├── test-library │ ├── Not in rekordbox │ │ ├── Not a Music File.txt │ │ └── Not in rekordbox.mp3 │ ├── PioneerDJ │ │ ├── Demo Tracks │ │ │ └── Demo Track 1.mp3 │ │ ├── Recording │ │ │ └── Demo Track 2.mp3 │ │ └── Sampler │ │ │ └── OSC_SAMPLER │ │ │ ├── PRESET ONESHOT │ │ │ └── SIREN.wav │ │ │ ├── PRESET RELOCATED DUPLICATE 1 │ │ │ └── SINEWAVE.wav │ │ │ ├── PRESET RELOCATED DUPLICATE 2 │ │ │ └── SINEWAVE.wav │ │ │ └── PRESET RELOCATED │ │ │ └── HORN.wav │ └── Special Filenames │ │ └── Name with #.mp3 └── xml-templates │ ├── broken-library.xml │ ├── expected-fixed-library-repaired-only.xml │ ├── expected-fixed-library.xml │ └── missing-attribute-library.xml └── scala └── com └── vividlab └── rekordbox ├── ConfigTest.scala ├── TestData.scala ├── TestUtils.scala ├── analyse └── AnalyserTest.scala └── repair └── RepairerTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # SBT 5 | .bsp/ 6 | 7 | target/ 8 | 9 | # IntelliJ IDEA 10 | .idea_modules/ 11 | .idea/ 12 | *.iml 13 | 14 | # MacOS 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ed Kennard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang2057{\fonttbl{\f0\fnil\fcharset0 Calibri;}} 2 | {\*\generator Riched20 10.0.17134}\viewkind4\uc1 3 | \pard\box\brdrdash\brdrw0 \sa200\sl276\slmult1\b\f0\fs18\lang9 MIT License\b0\line\line Copyright (c) 2019 Ed Kennard\line\line Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\line\line The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\line\line THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\line OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\fs22\par 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## rekordbox Repair 2 | 3 | ### Introduction 4 | rekordbox Repair is designed to help users of Pioneer's rekordbox DJ software fix problems with their rekordbox collections, as well as keep them clean on an ongoing basis. I wrote it while helping George Evelyn a.k.a. [Nightmares on Wax](https://nightmaresonwax.com) sort out his studio and DJ'ing computers. George has a macOS rekordbox collection of around 7200 tracks and counting, so it's against that this tool has initially been battle-tested. 5 | 6 | rekordbox Repair is a simple command line tool which works on both macOS and Windows. It analyses an exported rekordbox collection in XML format, searches the folder on disk where your music files live, then produces the following two files: 7 | 8 | 1. **A new rekordbox XML file** containing the repaired tracks with their correct file locations, original cue points and loops, and the playlists they belong to. This XML file can be loaded into rekordbox via its "rekordbox xml" area, and can be generated in two ways depending on how you want to repair your collection and playlists: 9 | - **Selectively repair tracks in your existing collection and playlists** - A new XML file is generated containing only the tracks the tool was able to repair, and the playlists they belong to. 10 | - **Rebuild your entire collection and all playlists** - A new XML file is generated containing your entire collection, including the tracks which the tool was able to repair. This option is useful if there's a large number of repaired tracks spread over many playlists which would be too time-consuming to fix using the first option above. 11 | 2. **A detailed report** listing the following issues the tool has detected: 12 | - Tracks which rekordbox is reporting as "File missing", but which have really just moved location on the disk and can therefore be repaired. 13 | - Tracks which genuinely do have their file missing, and should therefore be removed from rekordbox. 14 | - Files on disk which can't be imported to rekordbox because the full path to their location exceeds rekordbox's 255 character limit (only applies to macOS) 15 | - Files on disk which haven't been imported into rekordbox yet. 16 | 17 | ### Can't rekordbox fix missing files itself? 18 | In rekordbox 5, the version I was originally working against, there is a 'Relocate' tool for repairing tracks with missing files which operates either on a track-by-track basis, or on a hierarchy of folders and sub-folders assuming that the only thing that's moved is the top-most folder. For example, if you moved your /Users/You/Music/iTunes folder to a different location, e.g. /Users/You/**Dropbox**/Music/iTunes, without moving anything within the iTunes folder itself, then Relocate should work. 19 | 20 | Since I originally developed this tool, rekordbox 6 was released. The 'Relocate' tool still exists, plus a new 'Relocate All' tool has been added which is a big improvement and would ideally make my tool here redundant. Users can configure multiple library folders on their computer to scan, then any files which have been moved around will be repaired automatically. 21 | 22 | For now (May 2022), rekordbox Repair still offers at least two advantages: 23 | - rekordbox's 'Relocate All' tool ignores duplicate matches for a given track, and will just do a repair against the first match it finds, a risky strategy which could easily make a big mess. For example, if you have two albums, Artist 1/Album 1/Track 1.mp3 and Artist 2/Album 2/Track 1.mp3, rekordbox may incorrectly repair those, getting the `Track 1.mp3` mixed up between the two albums. rekordbox Repair will not attempt to repair this situation - instead you'll be told there were duplicates and be given a list of them, so you can deal with them manually. That can be a chore if there are a lot of duplicates, but it is a much safer approach. 24 | - rekordbox's 'Relocate All' tool doesn't give you a list of files on disk which haven't been imported into rekordbox yet. This is useful since you may have files lurking in your library which either should be in rekordbox but somehow got missed, or shouldn't be there and can be safely deleted. 25 | 26 | ### How do tracks in rekordbox end up with missing files? 27 | There are various reasons why rekordbox might have tracks with missing files: 28 | - If you've manually moved some music files or their folders around on your disk using Finder/Windows Explorer. 29 | - If you use a music library manager such as iTunes or Swinsian and have the "Keep library folder organised" option switched on. Whenever you edit a track's artist or album name, e.g. from "Incorrect Artist" to "Correct Artist" and from "Incorrect Album" to "Correct Album", the related file will be moved from the folder /Incorrect Artist/Incorrect Album to a new folder /Correct Artist/Correct Album, breaking rekordbox's reference to the file. 30 | - If you imported tracks directly from an external device (e.g. an external drive or USB stick), but that device isn't connected anymore. 31 | - The files have genuinely been deleted from your disk. 32 | 33 | ### How to install in macOS 34 | - Download the latest release from the [Releases](https://github.com/edkennard/rekordbox-repair/releases) page. 35 | - Double-click the downloaded file to extract the tool into a folder, e.g. `rekordbox-repair-0.3` 36 | - Move the extracted `rekordbox-repair-0.3` folder wherever you wish on your disk. The recommended location is `/Users/You/Applications/rekordbox-repair-0.3` 37 | - Check your installation is working by opening a Terminal window via Applications/Utilities/Terminal.app then enter two commands per below: 38 | ```bash 39 | DJLaptop:~ You$ cd "/Users/You/Applications/rekordbox-repair-0.3" 40 | 41 | DJLaptop:rekordbox-repair-0.3$ bin/rekordbox-repair --help 42 | ``` 43 | - The tool should output its help output per the "Help" section below. 44 | 45 | ### How to install in Windows 46 | - Download the latest release from the [Releases](https://github.com/edkennard/rekordbox-repair/releases) page. 47 | - Double-click the downloaded file then follow the steps in the setup wizard. 48 | - Check your installation is working by opening a Command Prompt window via Start Menu... Windows System... Command Prompt then enter this command: 49 | ```bash 50 | C:\Users\You> rekordbox-repair --help 51 | ``` 52 | - The tool should output its help output per the "Help" section below. 53 | 54 | ### Help 55 | Calling the tool with the "--help" option will give you the following info: 56 | ```bash 57 | Usage: rekordbox-repair [options] 58 | 59 | -i, --input-xml-file 60 | Input rekordbox XML file to analyse, e.g. '-i /Users/You/Documents/rekordbox/library.xml' 61 | -o, --output-xml-file 62 | Output rekordbox XML file to write repaired version to, e.g. '-o /Users/You/Documents/rekordbox/library-fixed.xml' 63 | -s, --search-directory 64 | Directory to search for missing files to relocate, e.g. '-s /Users/You/Music/iTunes' 65 | -r, --output-repaired-tracks-only 66 | Only write repaired tracks to the output rekordbox XML file, rather than the entire library. By default set to true, set to false if you want to write everything in order to rebuild your entire library 67 | --help Prints this usage text 68 | ``` 69 | 70 | ### How to use in macOS 71 | - Export your rekordbox collection to an XML file. This can be done via rekordbox's File menu... "Export Collection in xml format" tool. A recommended location to save this is `/Users/You/Documents/rekordbox/library.xml` 72 | - Open a Terminal window via Applications/Utilities/Terminal.app 73 | - Change to the rekordbox Repair folder (example assumes the location is `/Users/You/Applications/rekordbox-repair-0.3`: 74 | ```bash 75 | DJLaptop:~ You$ cd "/Users/You/Applications/rekordbox-repair-0.3" 76 | ``` 77 | - Run the tool, adjusting the values according to your own computer's disk locations: 78 | ```bash 79 | DJLaptop:rekordbox-repair-0.3 You$ bin/rekordbox-repair -i "/Users/You/Documents/rekordbox/original-library.xml" -o "/Users/You/Documents/rekordbox/fixed-library.xml" -s "/Users/You/Music/iTunes" 80 | ``` 81 | 82 | ### How to use in Windows 83 | - Export your rekordbox collection to an XML file. This can be done via rekordbox's File menu... "Export Collection in xml format" tool. A recommended location to save this is `C:\Users\You\Documents\rekordbox\library.xml` 84 | - Open a Command Prompt window via Start Menu... Windows System... Command Prompt 85 | - Run the tool, adjusting the values according to your own computer's files and folders: 86 | ```bash 87 | C:\Users\You> rekordbox-repair -i "C:\Users\You\Documents\rekordbox\original-library.xml" -o "C:\Users\You\Documents\rekordbox\fixed-library.xml" -s "C:\Users\You\Music\iTunes" 88 | ``` 89 | 90 | ### rekordbox Repair in action 91 | To demonstrate how this tool can be used to clean up a rekordbox collection, here is a working example with a deliberately broken collection and the complete sequence of steps that were taken to fix all the various issues. 92 | 93 | **Important note: Always make sure you have a complete and functioning backup of your computer before doing any work on your rekordbox collection.** 94 | 95 | #### Demo broken rekordbox collection 96 | ![rekordbox collection with missing files](/images/01_missing_files.png?raw=true "Collection with missing files") 97 | 98 | #### Demo broken rekordbox playlist 99 | ![rekordbox collection with missing files](/images/02_missing_files_in_playlist.png?raw=true "Playlist with missing files") 100 | 101 | #### Step 1: Exported collection and playlists to XML 102 | ![Export collection](/images/00_export_collection.png?raw=true "Export collection") 103 | 104 | #### Step 2: Ran rekordbox Repair against exported XML and iTunes music folder 105 | ```bash 106 | VividLab-Mac-Pro:rekordbox-repair-0.1 Ed$ bin/rekordbox-repair -i "/Users/Ed/Documents/rekordbox/original-library.xml" -o "/Users/Ed/Documents/rekordbox/fixed-library.xml" -s "/Users/Ed/Music/rekordbox iTunes/iTunes Media/Music" 107 | ``` 108 | 109 | #### rekordbox Repair's output 110 | ```text 111 | 2019-06-17 12:51:02,476 INFO - Running using Java version 1.8.0_211 112 | 2019-06-17 12:51:02,890 INFO - Analysing rekordbox collection... 113 | 2019-06-17 12:51:02,890 INFO - Reading rekordbox XML file /Users/Ed/Documents/rekordbox/original-library.xml... 114 | 2019-06-17 12:51:02,964 INFO - Loading tracks in the collection... 115 | 2019-06-17 12:51:02,983 INFO - Checking playlists... 116 | 2019-06-17 12:51:02,989 INFO - Locating files referenced by tracks in the collection... 117 | 2019-06-17 12:51:02,991 INFO - 'Nalin & Kane' - 'Essential Selection '97 - Winter - Pete Tong' - 'Beachball' - OK 118 | 2019-06-17 12:51:02,991 INFO - 'Solu Music' - 'Fade feat. KimBlee' - 'Fade feat. KimBlee (Original Mix (Part I))' - OK 119 | 2019-06-17 12:51:02,991 INFO - 'Way Out West' - 'Lullaby Horizon (Ben Bohmer Remix)' - 'Lullaby Horizon (Ben Bohmer Extended Mix)' - OK 120 | 2019-06-17 12:51:02,992 INFO - 'Akufen' - 'My Way' - 'Skidoos' - OK 121 | 2019-06-17 12:51:02,992 INFO - 'Trentemøller' - 'The Trentemøller Chronicles' - 'Les Dijinns (Trentemøller Remix)' - OK 122 | 2019-06-17 12:51:02,992 INFO - 'Jem Haynes & SOAME' - 'Streets EP' - 'Mountain Road' - OK 123 | 2019-06-17 12:51:02,992 INFO - 'Plain Pitts' - 'Requinto' - 'Requinto - Jay Shepheard Remix' - MISSING, searching for file 'Requinto - Jay Shepheard Remix.mp3'... 124 | 2019-06-17 12:51:03,062 INFO - 'Plain Pitts' - 'Requinto' - 'Requinto - Jay Shepheard Remix' - Found one matching filename, relocating in rekordbox to '/Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Plain Pits/Requinto/Requinto - Jay Shepheard Remix.mp3' 125 | 2019-06-17 12:51:03,062 INFO - 'Konstantin Sibold' - 'Secret Weapons EP Part 5' - 'Madeleine (Original Mix)' - OK 126 | 2019-06-17 12:51:03,062 INFO - 'Someone Else' - 'Pillowface' - 'Pillowface (Original Mix)' - OK 127 | 2019-06-17 12:51:03,062 INFO - 'Pachanga Boys' - 'Girlcatcher' - 'Time (Original Mix)' - OK 128 | 2019-06-17 12:51:03,062 INFO - 'Capricorn' - 'In Order to Dance' - '20 Hz' - OK 129 | 2019-06-17 12:51:03,063 INFO - 'Nathan Fake' - 'Outhouse Remixes Part 2' - 'Outhouse - Valentino Kanzyani Remix' - OK 130 | 2019-06-17 12:51:03,063 INFO - 'Orbital' - 'Orbital II' - 'Halcyon + On + On' - OK 131 | 2019-06-17 12:51:03,063 INFO - 'Tsunami One and BT' - 'Singles' - 'Hip Hop Phenomenon (Bassbin Twins Edit)' - MISSING, searching for file '01 Hip Hop Phenomenon (Bassbin Twins Edit).mp3'... 132 | 2019-06-17 12:51:03,070 WARN - 'Tsunami One and BT' - 'Singles' - 'Hip Hop Phenomenon (Bassbin Twins Edit)' - Couldn't find '01 Hip Hop Phenomenon (Bassbin Twins Edit).mp3', track will remain in rekordbox as a missing file 133 | 2019-06-17 12:51:03,071 INFO - 'Nightmares On Wax' - 'Mind Elevation' - 'Date With Destiny' - OK 134 | 2019-06-17 12:51:03,071 INFO - 'Minnie Riperton' - 'Defected In The House - Louis Vega' - 'Inside My Love' - MISSING, searching for file '3-09 Inside My Love.m4a'... 135 | 2019-06-17 12:51:03,078 INFO - 'Minnie Riperton' - 'Defected In The House - Louis Vega' - 'Inside My Love' - Found one matching filename, relocating in rekordbox to '/Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Compilations/Defected In The House - Louie Vega/3-09 Inside My Love.m4a' 136 | 2019-06-17 12:51:03,078 INFO - 'Stanton Warriors' - 'Stanton Remixed' - 'Who Are The Warriors - Basskleph Remix' - MISSING, searching for file '01 Who Are The Warriors - Basskleph Remix.mp3'... 137 | 2019-06-17 12:51:03,088 WARN - 'Stanton Warriors' - 'Stanton Remixed' - 'Who Are The Warriors - Basskleph Remix' - Found more than one matching filename so it's not safe to automatically relocate. Matches were: 138 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Stanton Warriors/Stantons Remixed/01 Who Are The Warriors - Basskleph Remix.mp3 139 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Stanton Warriors/Stantons Remixed Duplicate/01 Who Are The Warriors - Basskleph Remix.mp3 140 | 2019-06-17 12:51:03,096 INFO - Checking for files in the search directory which don't exist in rekordbox... 141 | 2019-06-17 12:51:03,117 INFO - 2 files in the search directory don't exist in rekordbox: 142 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Anti Serum/Singles/01 Bang Tha Drums.mp3 143 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Blunted Dummies With A Longer Name Than Necessary/Wild Pitch - The Story - Someone Went Nuts Naming The Album To Try And Pack As Much Info Into It/01 Blunted Dummies - House For All (DJ Pierre Wild PiTcH Mix).mp3 144 | 2019-06-17 12:51:03,118 INFO - Checking for files with a path too long for rekordbox... 145 | 2019-06-17 12:51:03,127 INFO - 1 file paths are too long for rekordbox (255 character limit): 146 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Blunted Dummies With A Longer Name Than Necessary/Wild Pitch - The Story - Someone Went Nuts Naming The Album To Try And Pack As Much Info Into It/01 Blunted Dummies - House For All (DJ Pierre Wild PiTcH Mix).mp3 147 | 2019-06-17 12:51:03,128 INFO - Finished analysing rekordbox collection 148 | 2019-06-17 12:51:03,132 INFO - 149 | ************* Results Summary ************* 150 | Total tracks in collection: 17 151 | Tracks OK: 13 152 | Tracks repaired: 2 153 | Tracks with multiple matches: 1 154 | Tracks with missing files: 1 155 | Tracks with path too long: 1 156 | Tracks on disk but not in rekordbox: 2 157 | ******************************************* 158 | 2019-06-17 12:51:03,133 INFO - Generating repaired rekordbox collection... 159 | 2019-06-17 12:51:03,146 INFO - Transforming XML... 160 | 2019-06-17 12:51:03,233 INFO - Saving new XML to /Users/Ed/Documents/rekordbox/fixed-library.xml... 161 | 2019-06-17 12:51:03,255 INFO - Finished generating repaired rekordbox collection 162 | 2019-06-17 12:51:03,268 INFO - For a detailed report, see here: /Users/Ed/Documents/rekordbox/fixed-library.report.txt 163 | 2019-06-17 12:51:03,268 INFO - Completed successfully 164 | ``` 165 | 166 | #### rekordbox Repair's detailed report (saved as "fixed-library.report.txt") 167 | ```text 168 | rekordbox Repair Report 169 | 170 | 171 | Summary 172 | 173 | Total tracks in collection: 17 174 | Tracks OK: 13 175 | Tracks repaired: 2 176 | Tracks with multiple matches: 1 177 | Tracks with missing files: 1 178 | Tracks with path too long: 1 179 | Tracks on disk but not in rekordbox: 2 180 | 181 | 182 | Repaired tracks 183 | 184 | The original files for the following tracks were not found in their expected locations, but were found elsewhere and repaired versions have been written to the output XML file: 185 | 186 | 'Minnie Riperton' - 'Defected In The House - Louis Vega' - 'Inside My Love' 187 | Old: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Compilations/Defected In The House - Louis Vega/3-09 Inside My Love.m4a 188 | New: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Compilations/Defected In The House - Louie Vega/3-09 Inside My Love.m4a 189 | 190 | 'Plain Pitts' - 'Requinto' - 'Requinto - Jay Shepheard Remix' 191 | Old: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Plain Pitts/Requinto/Requinto - Jay Shepheard Remix.mp3 192 | New: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Plain Pits/Requinto/Requinto - Jay Shepheard Remix.mp3 193 | 194 | 195 | Tracks with multiple matches 196 | 197 | The original files for the following tracks were not found in their expected locations, but multiple files with the same name were found elsewhere: 198 | 199 | 'Stanton Warriors' - 'Stanton Remixed' - 'Who Are The Warriors - Basskleph Remix' 200 | Old: 201 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Stanton Warriors/Stanton Remixed/01 Who Are The Warriors - Basskleph Remix.mp3 202 | 203 | New (Potentials): 204 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Stanton Warriors/Stantons Remixed/01 Who Are The Warriors - Basskleph Remix.mp3 205 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Stanton Warriors/Stantons Remixed Duplicate/01 Who Are The Warriors - Basskleph Remix.mp3 206 | 207 | 208 | Tracks with missing files 209 | 210 | The files for the following tracks couldn't be found anywhere in the specified search directory: 211 | 212 | 'Tsunami One and BT' - 'Singles' - 'Hip Hop Phenomenon (Bassbin Twins Edit)' 213 | Missing: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Tsunami One and BT/Singles/01 Hip Hop Phenomenon (Bassbin Twins Edit).mp3 214 | 215 | 216 | Tracks with path too long 217 | 218 | rekordbox has a limit of 255 characters for the full path to a track's file, e.g. "/Users/User/Music/Library/Artist/Album/Music File.mp3" is 53 characters long. The following files exceed that limit and therefore can't be imported to rekordbox: 219 | 220 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Blunted Dummies With A Longer Name Than Necessary/Wild Pitch - The Story - Someone Went Nuts Naming The Album To Try And Pack As Much Info Into It/01 Blunted Dummies - House For All (DJ Pierre Wild PiTcH Mix).mp3 221 | 222 | 223 | Tracks on disk but not in rekordbox 224 | 225 | The following files exist in the specified search directory but haven't yet been imported into rekordbox: 226 | 227 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Anti Serum/Singles/01 Bang Tha Drums.mp3 228 | /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Blunted Dummies With A Longer Name Than Necessary/Wild Pitch - The Story - Someone Went Nuts Naming The Album To Try And Pack As Much Info Into It/01 Blunted Dummies - House For All (DJ Pierre Wild PiTcH Mix).mp3 229 | ``` 230 | 231 | #### Step 3: Used report to fix issues that needed manual intervention 232 | - Per report's "Tracks with multiple matches" section, using Finder deleted duplicate Stanton Warriors track "01 Who Are The Warriors - Basskleph Remix.mp3" 233 | - Per report's "Tracks with path too long" section, using iTunes edited the Blunted Dummies track's artist name from "Blunted Dummies With A Longer Name Than Necessary" to "Blunted Dummies", and updated album name from "Wild Pitch - The Story - Someone Went Nuts Naming The Album To Try And Pack As Much Info Into It" to "Wild Pitch - The Story". Now with a shorter path, I was able to import this track into rekordbox successfully. 234 | - Per report's "Tracks on disk but not in rekordbox" section, imported Anti-Serum's "Bang Tha Drums" track into rekordbox 235 | - After fixing those issues, repeated steps 1 and 2 above. The refreshed report was already looking better: 236 | ```text 237 | rekordbox Repair Report 238 | 239 | 240 | Summary 241 | 242 | Total tracks in collection: 19 243 | Tracks OK: 15 244 | Tracks repaired: 3 245 | Tracks with multiple matches: 0 246 | Tracks with missing files: 1 247 | Tracks with path too long: 0 248 | Tracks on disk but not in rekordbox: 0 249 | 250 | 251 | Repaired tracks 252 | 253 | The original files for the following tracks were not found in their expected locations, but were found elsewhere and repaired versions have been written to the output XML file: 254 | 255 | 'Minnie Riperton' - 'Defected In The House - Louis Vega' - 'Inside My Love' 256 | Old: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Compilations/Defected In The House - Louis Vega/3-09 Inside My Love.m4a 257 | New: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Compilations/Defected In The House - Louie Vega/3-09 Inside My Love.m4a 258 | 259 | 'Plain Pitts' - 'Requinto' - 'Requinto - Jay Shepheard Remix' 260 | Old: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Plain Pitts/Requinto/Requinto - Jay Shepheard Remix.mp3 261 | New: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Plain Pits/Requinto/Requinto - Jay Shepheard Remix.mp3 262 | 263 | 'Stanton Warriors' - 'Stanton Remixed' - 'Who Are The Warriors - Basskleph Remix' 264 | Old: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Stanton Warriors/Stanton Remixed/01 Who Are The Warriors - Basskleph Remix.mp3 265 | New: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Stanton Warriors/Stantons Remixed/01 Who Are The Warriors - Basskleph Remix.mp3 266 | 267 | 268 | Tracks with missing files 269 | 270 | The files for the following tracks couldn't be found anywhere in the specified search directory: 271 | 272 | 'Tsunami One and BT' - 'Singles' - 'Hip Hop Phenomenon (Bassbin Twins Edit)' 273 | Missing: /Users/Ed/Music/rekordbox iTunes/iTunes Media/Music/Tsunami One and BT/Singles/01 Hip Hop Phenomenon (Bassbin Twins Edit).mp3 274 | ``` 275 | 276 | #### Step 4: Imported repaired tracks 277 | - In rekordbox, File menu... Display All Missing Files window, deleted all of them since for 3 tracks we have repaired versions ready to re-import, and for 1 track the file is genuinely gone from the iTunes music folder. **It's important to delete broken tracks from the collection before importing the repaired versions**. 278 | - In rekordbox Preferences window... View... Layout... section, enabled "rekordbox xml" 279 | - In rekordbox Preferences window... Advanced... rekordbox xml section, pointed the "Imported Library" to the "fixed-library.xml" file generated by rekordbox Repair: 280 | 281 | ![rekordbox xml preferences](/images/04_rekordbox_xml_prefs.png?raw=true "rekordbox xml Preferences") 282 | 283 | - Opened the "rekordbox xml" area in the main window to see the 3 repaired tracks listed, with cue points intact, and also the two playlists these tracks belonged to: 284 | 285 | ![rekordbox xml tracks](/images/05_rekordbox_xml_tracks.png?raw=true "rekordbox xml Tracks") 286 | 287 | ![rekordbox xml playlist](/images/06_rekordbox_xml_playlist.png?raw=true "rekordbox xml Playlist") 288 | 289 | - Drag-dropped all 3 repaired tracks from "rekordbox xml"... "All Tracks" into the main collection 290 | - For each playlist in the "rekordbox xml" area, drag-dropped the repaired tracks back into the original playlists 291 | - Selected all tracks in the main rekordbox collection, right-clicked, then selected "Reload Tags". This ensured that any artist and album names which had been renamed in iTunes were reflected correctly in rekordbox. 292 | 293 | #### Repaired collection and playlists 294 | 295 | ![repaired collection](/images/07_repaired_collection.png?raw=true "Repaired collection") 296 | 297 | ![repaired playlist](/images/08_repaired_playlist.png?raw=true "Repaired playlist") 298 | 299 | #### Final rekordbox Repair report 300 | After fixing everything, repeated steps 1 and 2 above. At that point rekordbox Repair reported no issues: 301 | ```text 302 | ************* Results Summary ************* 303 | Total tracks in collection: 18 304 | Tracks OK: 18 305 | Tracks repaired: 0 306 | Tracks with multiple matches: 0 307 | Tracks with missing files: 0 308 | Tracks with path too long: 0 309 | Tracks on disk but not in rekordbox: 0 310 | ******************************************* 311 | ``` 312 | 313 | ### Future plans 314 | - In the interest of expediency I wrote this initial version as a command line tool only, but with more time it would be better to build a full-blown user interface for it and package as a macOS app which can be installed into the main Applications area, and a Windows app which can be installed into the Start Menu etc. 315 | - Add support for specifying multiple search directories rather than only one. For some people their collections exist across multiple disks. 316 | - Implement a more intelligent search for missing files where the file may have changed name but it can still be matched based on other criteria, e.g. file size, metadata, the names of its parent folders, the Levenshtein distance between the original filename and the new filename, etc. 317 | - Is there anything you think should be added, changed or fixed? Please submit your thoughts to the [Issues](https://github.com/edkennard/rekordbox-repair/issues) area. 318 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import NativePackagerHelper._ 2 | 3 | enablePlugins(SbtLicenseReport, JavaAppPackaging, WindowsPlugin) 4 | 5 | name := "rekordbox-repair" 6 | version := "0.5" 7 | scalaVersion := "2.13.8" 8 | scalacOptions ++= Seq( 9 | "-deprecation", 10 | "-feature", 11 | "-unchecked", 12 | "-Xfatal-warnings", 13 | ) 14 | 15 | libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "2.1.0" 16 | libraryDependencies += "com.github.scopt" %% "scopt" % "4.0.1" 17 | libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.11" 18 | libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.11" % Test 19 | 20 | // Packaging config and mappings 21 | // 22 | // On MacOS run "sbt clean test dumpLicenseReport universal:packageZipTarball", which will keep executable permissions intact where packageBin doesn't 23 | // On Windows run "sbt clean test dumpLicenseReport windows:packageBin" to generate a WIX-based setup wizard 24 | 25 | maintainer := "Ed Kennard " 26 | packageSummary := "rekordbox Repair Tool" 27 | packageDescription := """Command line tool to help users of Pioneer's rekordbox DJ software clean up their collections then keep them that way""" 28 | 29 | val osName = System.getProperty("os.name").toLowerCase 30 | val isMac = osName.startsWith("mac") 31 | val isWindows = osName.startsWith("win") 32 | 33 | Universal / mappings ++= { 34 | val jreDir = if (isMac) 35 | Path("/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre").asFile 36 | else if (isWindows) 37 | Path("C:\\Program Files\\Java\\jdk1.8.0_211\\jre").asFile 38 | else 39 | throw new IllegalArgumentException("Packaging is only configured for MacOS and Windows, since rekordbox only exists on those platforms") 40 | 41 | if (!jreDir.exists) 42 | throw new IllegalArgumentException("The required JDK is not present on this system - please install JDK 1.8.0_211") 43 | 44 | streams.value.log.info(s"Adding JRE to package from $jreDir...") 45 | 46 | directory(jreDir) 47 | } 48 | 49 | Universal / mappings ++= { 50 | val licenseReportsDir = target.value / "license-reports" 51 | streams.value.log.info(s"Adding license reports to package from $licenseReportsDir...") 52 | directory(licenseReportsDir) 53 | } 54 | 55 | 56 | // For non-Windows packages set JAVA_HOME to use our packaged JRE in /jre 57 | val nonWindowsJavaHome = if (!isWindows) 58 | Seq("-java-home ${app_home}/../jre") 59 | else 60 | Seq() 61 | 62 | Universal / javaOptions ++= nonWindowsJavaHome 63 | 64 | 65 | // Windows packaging using WIX toolset 66 | Windows / name := s"rekordbox-repair-${version.value}" // Name of generated MSI file 67 | wixProductLicense := Some(new sbt.File("LICENSE.rtf")) 68 | 69 | makeBatScripts := { 70 | // Custom .bat script generation to set JAVA_HOME on Windows package to use our packaged JRE in /jre 71 | // Thanks to solution found here: https://github.com/sbt/sbt-native-packager/issues/1070 72 | val batScripts = makeBatScripts.value 73 | val log = streams.value.log 74 | 75 | batScripts.map(_._1).foreach{batScript => 76 | log.info(s"Updating $batScript to use our packaged JRE...") 77 | 78 | val newLines = IO.readLines(batScript).flatMap { 79 | case s @ "set \"APP_LIB_DIR=%APP_HOME%\\lib\\\"" => 80 | Seq(s, "set \"JAVA_HOME=%APP_HOME%\\jre\"") 81 | case s => Seq(s) 82 | } 83 | 84 | IO.writeLines(batScript, newLines) 85 | log.info(s"Successfully updated $batScript.") 86 | } 87 | 88 | batScripts 89 | } 90 | -------------------------------------------------------------------------------- /images/00_export_collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/images/00_export_collection.png -------------------------------------------------------------------------------- /images/01_missing_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/images/01_missing_files.png -------------------------------------------------------------------------------- /images/02_missing_files_in_playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/images/02_missing_files_in_playlist.png -------------------------------------------------------------------------------- /images/03_missing_file_manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/images/03_missing_file_manager.png -------------------------------------------------------------------------------- /images/04_rekordbox_xml_prefs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/images/04_rekordbox_xml_prefs.png -------------------------------------------------------------------------------- /images/05_rekordbox_xml_tracks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/images/05_rekordbox_xml_tracks.png -------------------------------------------------------------------------------- /images/06_rekordbox_xml_playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/images/06_rekordbox_xml_playlist.png -------------------------------------------------------------------------------- /images/07_repaired_collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/images/07_repaired_collection.png -------------------------------------------------------------------------------- /images/08_repaired_playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/images/08_repaired_playlist.png -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.6.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0") 2 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.5") 3 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logs/rekordbox-repair.log 5 | true 6 | true 7 | 8 | %d{ISO8601} %-5level - %msg%n 9 | 10 | 11 | INFO 12 | 13 | 14 | 15 | 16 | true 17 | 18 | %d{ISO8601} %-5level - %msg%n 19 | 20 | 21 | INFO 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/resources/report-template.txt: -------------------------------------------------------------------------------- 1 | rekordbox Repair Report 2 | 3 | 4 | Summary 5 | {summary} 6 | 7 | 8 | Repaired tracks 9 | 10 | The original files for the following tracks were not found in their expected locations, but were found elsewhere and repaired versions have been written to the output XML file: 11 | 12 | {tracks-repaired} 13 | 14 | 15 | Tracks with multiple matches 16 | 17 | The original files for the following tracks were not found in their expected locations, but multiple files with the same name were found elsewhere: 18 | 19 | {tracks-multiple-matches} 20 | 21 | 22 | Tracks with missing files 23 | 24 | The files for the following tracks couldn't be found anywhere in the specified search directory: 25 | 26 | {tracks-missing} 27 | 28 | 29 | Tracks with invalid locations 30 | 31 | The following tracks couldn't be analysed or repaired because the file locations specified in the rekordbox XML library file were in some way invalid, e.g. contained a raw question mark. For these files the fix is to remove them from your rekordbox collection then re-import them: 32 | 33 | {tracks-invalid-path} 34 | 35 | 36 | Tracks with path too long 37 | 38 | rekordbox has a limit of 255 characters for the full path to a track's file, e.g. "/Users/User/Music/Library/Artist/Album/Music File.mp3" is 53 characters long. The following files exceed that limit and therefore can't be imported to rekordbox: 39 | 40 | {tracks-path-too-long} 41 | 42 | 43 | Tracks on disk but not in rekordbox 44 | 45 | The following files exist in the specified search directory but haven't yet been imported into rekordbox: 46 | 47 | {tracks-not-in-rekordbox} 48 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/Config.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox 2 | 3 | import java.io.File 4 | 5 | case class Config( 6 | inputXmlFile_ : Option[File] = None, 7 | outputXmlFile_ : Option[File] = None, 8 | searchDirectory_ : Option[File] = None, 9 | outputRepairedTracksOnly: Boolean = true 10 | ) { 11 | def inputXmlFile: File = inputXmlFile_.getOrElse { 12 | throw new IllegalStateException("Input rekordbox XML file not provided") 13 | } 14 | def outputXmlFile: File = outputXmlFile_.getOrElse { 15 | throw new IllegalStateException("Output rekordbox XML file not provided") 16 | } 17 | def searchDirectory: File = searchDirectory_.getOrElse { 18 | throw new IllegalStateException("Search directory not provided") 19 | } 20 | } 21 | 22 | case object Config { 23 | 24 | private val optionsParser = new scopt.OptionParser[Config]("rekordbox-repair") { 25 | opt[File]('i', "input-xml-file") valueName "" action { (x, c) => 26 | c.copy(inputXmlFile_ = Some(x)) 27 | } text "Input rekordbox XML file to analyse, e.g. '-i /Users/You/Documents/rekordbox/library.xml'" 28 | 29 | opt[File]('o', "output-xml-file") valueName "" action { (x, c) => 30 | c.copy(outputXmlFile_ = Some(x)) 31 | } text "Output rekordbox XML file to write fixed version to, e.g. '-o /Users/You/Documents/rekordbox/library-fixed.xml'" 32 | 33 | opt[File]('s', "search-directory") valueName "" action { (x, c) => 34 | c.copy(searchDirectory_ = Some(x)) 35 | } text "Directory to search for missing files to relocate, e.g. '-s /Users/You/Music/iTunes'" 36 | 37 | opt[Boolean]('r', "output-repaired-tracks-only") valueName "" action { (x, c) => 38 | c.copy(outputRepairedTracksOnly = x) 39 | } text "Only write repaired tracks to the output rekordbox XML file, rather than the entire library. By default set to true, set to false if you want to write everything in order to rebuild your entire library" 40 | 41 | help("help") text "Prints this usage text" 42 | } 43 | 44 | def apply(args: Array[String]): Config = optionsParser.parse(args, Config()) match { 45 | case Some(config) => 46 | if (!config.inputXmlFile.exists) 47 | throw new IllegalArgumentException(s"Input rekordbox XML file doesn't exist: ${config.inputXmlFile.getAbsolutePath}") 48 | 49 | if (!config.searchDirectory.exists) 50 | throw new IllegalArgumentException(s"Directory to search doesn't exist: ${config.searchDirectory.getAbsolutePath}") 51 | 52 | config 53 | 54 | case None => 55 | throw new IllegalStateException(s"Failed to read configuration: ${args.mkString(", ")}") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/FileUtils.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox 2 | 3 | import com.vividlab.rekordbox.analyse.AnalyserResult 4 | import org.slf4j.LoggerFactory 5 | 6 | import java.io.{BufferedWriter, File, FileWriter} 7 | import java.net.URI 8 | import java.nio.file.AccessDeniedException 9 | 10 | object FileUtils { 11 | private val log = LoggerFactory.getLogger(getClass) 12 | 13 | val supportedTypes = Seq("mp3", "m4a", "aac", "aif", "aiff", "fla", "flac", "wav", "avi", "mpg", "mp4", "m4v", "mov", "qtz") 14 | 15 | /** 16 | * A location in rekordbox format ready to write out to XML with any special characters such as spaces encoded 17 | * to %20, and the rekordbox non-standard format for the host 'localhost': 18 | * 19 | * file://localhost/Users/User/Music/PioneerDJ/Demo%20Tracks/Demo%20Track%202.mp3 20 | * file://localhost/Users/User/Music/Name%20with%20#.mp3 21 | */ 22 | def locationFromFile(file: File): String = 23 | file.toURI.toString 24 | .replace("file:", "file://localhost") 25 | .replace("%23", "#") 26 | 27 | /** 28 | * A File object converted from a rekordbox location, with special characters decoded. 29 | * Hashes need special treatment as they pass through the URI. 30 | * 31 | * file:///Users/User/Music/PioneerDJ/Demo Tracks/Demo Track 2.mp3 32 | * file:///Users/User/Music/Name with #.mp3 33 | * 34 | * @param location A location in rekordbox format extracted from XML 35 | */ 36 | def fileFromLocation(location: String): Option[File] = { 37 | try { 38 | Some(new File(new URI(location 39 | .replace("file://localhost/", "file:///") 40 | .replace("#", "%23") 41 | ))) 42 | } catch { 43 | case e: Throwable => 44 | // Swallow exceptions while trying to read them from the given location - urls with non-escaped characters have 45 | // been experienced e.g. question marks, and we don't want one malformed url to cause the whole process to fail 46 | log.error(s"Ignoring invalid file location $location, failed to read with error: ${e.getMessage}") 47 | None 48 | } 49 | } 50 | 51 | /** 52 | * Safely retrieve the file system path for a given file, swallowing any exceptions and outputting them to the console 53 | */ 54 | def fileCanonicalPath(file: File): Option[String] = { 55 | try { 56 | Some(file.getCanonicalPath) 57 | } catch { 58 | case e: Throwable => 59 | log.error(s"Ignoring file ${file.getName}, failed to read its path on the filesystem: ${e.getMessage}") 60 | None 61 | } 62 | } 63 | 64 | /** 65 | * List all files in the given directory and all of its subdirectories. 66 | * 67 | * This could be refined to inspect the actual files for details like encoding method, bit depth and sampling frequency. 68 | * For more detail on rekordbox's supported formats, see pages 196 and 197 of https://rekordbox.com/_app/files/img/rekordbox5.5.0_manual_EN.pdf 69 | */ 70 | def allSupportedFilesInDir(rootDir: File): Seq[File] = { 71 | def supported(filename: String): Boolean = supportedTypes.exists { st => filename.toLowerCase.endsWith(s".$st") } 72 | 73 | def recursiveFilesInDir(dir: File): Seq[File] = { 74 | val files = try { 75 | dir.listFiles.toIndexedSeq 76 | } catch { 77 | case _: AccessDeniedException => 78 | log.info(s"Ignoring directory ${dir.getAbsolutePath}, access was denied") 79 | Nil 80 | case e: Throwable => 81 | log.info(s"Ignoring directory ${dir.getAbsolutePath}, read failed with error '$e'") 82 | Nil 83 | } 84 | // We could also filter by canRead here, in addition to isDirectory, but it's better to catch exceptions above and 85 | // report them to the user, in case a directory is being ignored which they would expect to be included 86 | files.filter(f => supported(f.getName)) ++ files.filter(_.isDirectory).flatMap(recursiveFilesInDir) 87 | } 88 | recursiveFilesInDir(rootDir).filterNot(_.isDirectory) 89 | } 90 | 91 | def writeTextFile(file: File, text: String): Unit = { 92 | val writer = new BufferedWriter(new FileWriter(file)) 93 | writer.write(text) 94 | writer.close() 95 | } 96 | 97 | /** 98 | * From the given XML file, create a corresponding file for writing the detailed report. 99 | * 100 | * For example, if the output XML file is /rekordbox-fixed.xml, the report file will be /rekordbox-fixed.report.txt 101 | */ 102 | def reportFileFromXmlFile(xmlFile: File): File = { 103 | val xmlFilePath = xmlFile.getAbsolutePath 104 | val filename = s"${xmlFilePath.substring(0, xmlFilePath.lastIndexOf("."))}.report.txt" 105 | new File(filename) 106 | } 107 | 108 | def writeReport(reportFile: File, result: AnalyserResult): Unit = { 109 | FileUtils.writeTextFile(reportFile, result.reportText()) 110 | log.info(s"For a detailed report, see here: ${reportFile.getAbsolutePath}") 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/Main.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox 2 | 3 | import com.vividlab.rekordbox.analyse.Analyser 4 | import com.vividlab.rekordbox.repair.Repairer 5 | import org.slf4j.LoggerFactory 6 | 7 | object Main { 8 | private val log = LoggerFactory.getLogger(getClass) 9 | 10 | def main(args: Array[String]): Unit = { 11 | log.info(s"Running using Java version ${System.getProperty("java.version")}") 12 | 13 | val config = Config(args) 14 | 15 | Analyser.analyse(config) match { 16 | case Right(result) => 17 | log.info(result.logText()) 18 | 19 | Repairer.repair(config, result) 20 | 21 | FileUtils.writeReport(FileUtils.reportFileFromXmlFile(config.outputXmlFile), result) 22 | 23 | log.info("Completed successfully") 24 | case Left(e) => 25 | log.error(s"Failed to complete successfully: ${e.getMessage}", e) 26 | System.exit(-1) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/OS.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox 2 | 3 | object OS { 4 | private val osName = System.getProperty("os.name").toLowerCase 5 | val isMac: Boolean = osName.startsWith("mac") 6 | val isWindows: Boolean = osName.startsWith("windows") 7 | 8 | val newLine: String = sys.props("line.separator") 9 | val paragraph: String = newLine + newLine 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/analyse/Analyser.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox.analyse 2 | 3 | import java.io.File 4 | import java.nio.file.{Files, Paths} 5 | 6 | import com.vividlab.rekordbox.{Config, FileUtils, OS} 7 | import com.vividlab.rekordbox.data.{CollectionTrack, CollectionTracks, RootPlaylist} 8 | import org.slf4j.LoggerFactory 9 | 10 | import scala.jdk.CollectionConverters._ 11 | import scala.xml.XML 12 | 13 | object Analyser { 14 | private val log = LoggerFactory.getLogger(getClass) 15 | 16 | def analyse(config: Config): Either[Exception, AnalyserResult] = { 17 | try { 18 | log.info("Analysing rekordbox collection...") 19 | 20 | log.info(s"Reading rekordbox XML file ${config.inputXmlFile.getAbsolutePath}...") 21 | val inputXml = XML.loadFile(config.inputXmlFile) 22 | 23 | log.info(s"Loading tracks in the collection...") 24 | val tracks = CollectionTracks.fromXml(inputXml) 25 | 26 | // Check here that all playlists can be successfully read now to avoid delaying failure until later when transforming the XML 27 | log.info(s"Checking playlists...") 28 | RootPlaylist.fromXml(inputXml) 29 | 30 | log.info(s"Listing audio and video files in the search directory (${FileUtils.supportedTypes.mkString(", ")})...") 31 | val allFilesInSearchDir = FileUtils.allSupportedFilesInDir(config.searchDirectory) 32 | log.info(s"Found ${allFilesInSearchDir.size} audio and video files in the search directory") 33 | 34 | log.info(s"Locating files referenced by tracks in the collection...") 35 | val locateResults = locateFiles(tracks, allFilesInSearchDir) 36 | 37 | log.info(s"Checking for files in the search directory which don't exist in rekordbox...") 38 | val notInRekordBox = filesNotInRekordBox(allFilesInSearchDir, locateResults.results) 39 | 40 | log.info(s"Checking for files with a path too long for rekordbox...") 41 | val tooLong = filesWithPathTooLong(allFilesInSearchDir) 42 | 43 | log.info("Finished analysing rekordbox collection") 44 | 45 | Right(AnalyserResult(locateResults, notInRekordBox, tooLong)) 46 | } catch { 47 | case e: Exception => 48 | Left(e) 49 | } 50 | } 51 | 52 | private def locateFiles(tracks: Seq[CollectionTrack], files: Seq[File]): LocateFileResults = { 53 | LocateFileResults( 54 | tracks.map(locateFile(_, files)) 55 | ) 56 | } 57 | 58 | private def locateFile(track: CollectionTrack, files: Seq[File]): LocateFileResult = { 59 | val logPrefix = track.toString 60 | 61 | (track.file, track.filePath) match { 62 | case (Some(file), Some(_)) => 63 | if (file.exists()) { 64 | log.info(s"$logPrefix - OK") 65 | OK(track) 66 | } else { 67 | log.info(s"$logPrefix - MISSING, searching for file '${file.getName}'...") 68 | val searchResults = files.filter(_.getName == file.getName) 69 | 70 | if (searchResults.isEmpty) { 71 | log.warn(s"$logPrefix - Couldn't find '${file.getName}', track will remain in rekordbox as a missing file") 72 | Missing(track) 73 | } else if (searchResults.size == 1) { 74 | val relocated = Relocated(track, searchResults.head) 75 | log.info(s"$logPrefix - Found one matching filename, relocating in rekordbox to '${searchResults.head.getCanonicalPath}'") 76 | relocated 77 | } else { 78 | val multiple = MultipleLocations(track, searchResults) 79 | log.warn(s"$logPrefix - Found more than one matching filename so it's not safe to automatically relocate. Matches were:${OS.newLine}${searchResults.map(_.getCanonicalPath).mkString(OS.newLine)}") 80 | multiple 81 | } 82 | } 83 | case _ => 84 | Invalid(track) 85 | } 86 | } 87 | 88 | /** 89 | * Builds a list of the files which are in some way already referenced by rekordbox, either by being a track in the 90 | * original collection, or a track that is in the process of being repaired by this tool or has been flagged as having 91 | * multiple potential new locations 92 | */ 93 | private def filesNotInRekordBox(allFilesInSearchDir: Seq[File], locateResults: Seq[LocateFileResult]): Seq[File] = { 94 | 95 | def filesFromResult(result: LocateFileResult): Seq[File] = { 96 | result match { 97 | case r: Relocated => 98 | Seq(r.newFile) 99 | case m: MultipleLocations => 100 | m.newFiles 101 | case _ => 102 | // Not interested in the other result types, e.g. OK / Missing 103 | Seq() 104 | } 105 | } 106 | val collectionFiles = locateResults.flatMap(_.track.file) 107 | val locateResultFiles = locateResults.flatMap(result => filesFromResult(result)) 108 | 109 | val notInRekordBox = allFilesInSearchDir.filterNot((collectionFiles ++ locateResultFiles).contains) 110 | 111 | if (notInRekordBox.nonEmpty) 112 | log.info(s"${notInRekordBox.size} files in the search directory don't exist in rekordbox:${OS.newLine}${notInRekordBox.mkString(OS.newLine)}") 113 | 114 | notInRekordBox 115 | } 116 | 117 | /** 118 | * rekordbox has a maximum file path of 255 characters, which can easily be exceeded by a rogue album or track 119 | * name. Any files on disk exceeding this limit will never make it into rekordbox 120 | */ 121 | private def filesWithPathTooLong(allFilesInSearchDir: Seq[File]): Seq[File] = { 122 | val tooLong = allFilesInSearchDir.filter(_.getAbsolutePath.length > 256) 123 | 124 | if (tooLong.nonEmpty) 125 | log.info(s"${tooLong.size} file paths are too long for rekordbox (255 character limit):${OS.newLine}${tooLong.mkString(OS.newLine)}") 126 | 127 | tooLong 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/analyse/AnalyserResult.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox.analyse 2 | 3 | import java.io.File 4 | 5 | import com.vividlab.rekordbox.OS 6 | 7 | import scala.io.Source 8 | 9 | case class AnalyserResult( 10 | locateFileResults: LocateFileResults, 11 | filesNotInRekordBox: Seq[File], 12 | filesWithPathTooLong: Seq[File] 13 | ) { 14 | val total: Int = locateFileResults.results.size 15 | 16 | val summary: String = s""" 17 | |Total tracks in collection: $total 18 | |Tracks OK: ${locateFileResults.ok.size} 19 | |Tracks repaired: ${locateFileResults.relocated.size} 20 | |Tracks with multiple matches: ${locateFileResults.multipleLocations.size} 21 | |Tracks with missing files: ${locateFileResults.missing.size} 22 | |Tracks with invalid paths: ${locateFileResults.invalid.size} 23 | |Tracks with path too long: ${filesWithPathTooLong.size} 24 | |Tracks on disk but not in rekordbox: ${filesNotInRekordBox.size} 25 | |""".stripMargin 26 | 27 | def logText(): String = 28 | s"${OS.newLine}************* Results Summary *************$summary*******************************************" 29 | 30 | 31 | def reportText(): String = 32 | Source.fromResource(s"report-template.txt").getLines().mkString(OS.newLine) 33 | .replace("{summary}", summary) 34 | .replace("{tracks-repaired}", locateFileResults.relocated.map(_.toString).mkString(OS.newLine)) 35 | .replace("{tracks-multiple-matches}", locateFileResults.multipleLocations.map(_.toString).mkString(OS.newLine)) 36 | .replace("{tracks-missing}", locateFileResults.missing.map(_.toString).mkString(OS.newLine)) 37 | .replace("{tracks-invalid-path}", locateFileResults.invalid.map(_.toString).mkString(OS.newLine)) 38 | .replace("{tracks-path-too-long}", filesWithPathTooLong.map(_.getAbsolutePath).mkString(OS.newLine)) 39 | .replace("{tracks-not-in-rekordbox}", filesNotInRekordBox.map(_.getAbsolutePath).mkString(OS.newLine)) 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/analyse/LocateFileResult.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox.analyse 2 | 3 | import java.io.File 4 | 5 | import com.vividlab.rekordbox.OS 6 | import com.vividlab.rekordbox.data.CollectionTrack 7 | 8 | import scala.reflect.ClassTag 9 | 10 | sealed trait LocateFileResult { 11 | def track: CollectionTrack 12 | } 13 | 14 | case class OK(track: CollectionTrack) extends LocateFileResult 15 | 16 | case class Invalid(track: CollectionTrack) extends LocateFileResult { 17 | override def toString: String = { 18 | s"$track${OS.newLine}Invalid: ${track.location}${OS.newLine}" 19 | } 20 | } 21 | 22 | case class Missing(track: CollectionTrack) extends LocateFileResult { 23 | override def toString: String = { 24 | s"$track${OS.newLine}Missing: ${track.filePath.getOrElse("File path missing")}${OS.newLine}" 25 | } 26 | } 27 | 28 | case class Relocated(track: CollectionTrack, newFile: File) extends LocateFileResult { 29 | override def toString: String = { 30 | s"$track${OS.newLine}Old: ${track.filePath.getOrElse("File path missing")}${OS.newLine}New: ${newFile.getAbsolutePath}${OS.newLine}" 31 | } 32 | } 33 | 34 | case class MultipleLocations(track: CollectionTrack, newFiles: Seq[File]) extends LocateFileResult { 35 | override def toString: String = { 36 | s"$track${OS.newLine}Old:${OS.newLine}${track.filePath.getOrElse("File path missing")}${OS.paragraph}New (Potentials):${OS.newLine}${newFiles.map(_.getAbsolutePath).mkString(OS.newLine)}" 37 | } 38 | } 39 | 40 | case class LocateFileResults(results: Seq[LocateFileResult]) { 41 | 42 | implicit class RichTraversable[A](collection: Iterable[A]) { 43 | def filterOnType[T <: A](implicit t: ClassTag[T]): Vector[T] = { 44 | collection.toVector.collect { case a: T => a } 45 | } 46 | } 47 | 48 | val ok: Seq[OK] = results.filterOnType[OK].sortBy(_.track.artist.toLowerCase) 49 | val relocated: Seq[Relocated] = results.filterOnType[Relocated].sortBy(_.track.artist.toLowerCase) 50 | val multipleLocations: Seq[MultipleLocations] = results.filterOnType[MultipleLocations].sortBy(_.track.artist.toLowerCase) 51 | val missing: Seq[LocateFileResult] = results.filterOnType[Missing].sortBy(_.track.artist.toLowerCase) 52 | val invalid: Seq[LocateFileResult] = results.filterOnType[Invalid].sortBy(_.track.artist.toLowerCase) 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/data/CollectionTrack.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox.data 2 | 3 | import java.io.File 4 | 5 | import com.vividlab.rekordbox.FileUtils 6 | 7 | import scala.xml.{Elem, Node} 8 | 9 | case class CollectionTrack( 10 | id: String, 11 | artist: String, 12 | album: String, 13 | name: String, 14 | location: String 15 | ) { 16 | override def toString: String = { 17 | val artistName = if (artist.nonEmpty) s"'$artist'" else "No Artist" 18 | val albumName = if (album.nonEmpty) s"'$album'" else "No Album" 19 | s"$artistName - $albumName - '$name'" 20 | } 21 | 22 | val file: Option[File] = FileUtils.fileFromLocation(location) 23 | val filePath: Option[String] = file.flatMap(FileUtils.fileCanonicalPath) 24 | } 25 | 26 | object CollectionTrack { 27 | def apply(node: Node): CollectionTrack = { 28 | (node.attribute("TrackID"), node.attribute("Artist"), node.attribute("Album"), node.attribute("Name"), node.attribute("Location")) match { 29 | case (Some(id), Some(artist), Some(album), Some(name), Some(location)) => 30 | CollectionTrack(id.text, artist.text, album.text, name.text, location.text) 31 | case _ => 32 | throw new IllegalArgumentException(s"Couldn't read a rekordbox track's XML, it was missing either a TrackID, Name, Artist or Location attribute: $node") 33 | } 34 | } 35 | } 36 | 37 | object CollectionTracks { 38 | def fromXml(xmlRoot: Elem): Seq[CollectionTrack] = xmlRoot \ "COLLECTION" \ "TRACK" map { CollectionTrack.apply } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/data/Playlist.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox.data 2 | 3 | import scala.xml.{Elem, Node, Text} 4 | 5 | trait Playlist { 6 | def name: String 7 | 8 | def containsTrack(trackId: String): Boolean = { 9 | def recursiveSearch(pl: Playlist): Boolean = pl match { 10 | case fpl: FolderPlaylist => 11 | fpl.children.exists(recursiveSearch) 12 | case tpl: TracksPlaylist => 13 | tpl.tracks.exists(_.trackId == trackId) 14 | } 15 | recursiveSearch(this) 16 | } 17 | } 18 | 19 | object Playlist { 20 | def apply(node: Node): Playlist = node.attribute("Type") match { 21 | case Some(typ: Text) => 22 | if (typ.text == "0") 23 | FolderPlaylist(node) 24 | else if (typ.text == "1") 25 | TracksPlaylist(node) 26 | else 27 | throw new IllegalArgumentException(s"Couldn't read a rekordbox playlist's XML, the Type attribute wasn't either 0 (folder) or 1 (tracks): $node") 28 | case _ => 29 | throw new IllegalArgumentException(s"Couldn't read a rekordbox playlist's XML, it was missing the Type attribute: $node") 30 | } 31 | } 32 | 33 | 34 | case class FolderPlaylist( 35 | name: String, 36 | children: Seq[Playlist] 37 | ) extends Playlist 38 | 39 | object FolderPlaylist { 40 | def apply(node: Node): FolderPlaylist = node.attribute("Name") match { 41 | case Some(name: Text) => 42 | FolderPlaylist(name.text, (node \ "NODE").map(Playlist.apply)) 43 | case _ => 44 | throw new IllegalArgumentException(s"Couldn't read a rekordbox playlist's XML, it was missing the Name attribute: $node") 45 | } 46 | } 47 | 48 | 49 | case class TracksPlaylist( 50 | name: String, 51 | tracks: Seq[PlaylistTrack] 52 | ) extends Playlist 53 | 54 | case object TracksPlaylist { 55 | def apply(node: Node): TracksPlaylist = node.attribute("Name") match { 56 | case Some(name) => 57 | TracksPlaylist(name.text, PlaylistTracks.fromXml(node)) 58 | case _ => 59 | throw new IllegalArgumentException(s"Couldn't read a rekordbox playlist's XML, it was missing the Name attribute: $node") 60 | } 61 | } 62 | 63 | 64 | object RootPlaylist { 65 | def fromXml(xmlRoot: Elem): FolderPlaylist = (xmlRoot \ "PLAYLISTS" \ "NODE").headOption match { 66 | case Some(rootPlaylistNode) => 67 | FolderPlaylist.apply(rootPlaylistNode) 68 | case None => 69 | throw new IllegalArgumentException(s"Couldn't read the rekordbox root playlist XML") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/data/PlaylistTrack.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox.data 2 | 3 | import scala.xml.Node 4 | 5 | case class PlaylistTrack( 6 | trackId: String 7 | ) 8 | 9 | object PlaylistTrack { 10 | def apply(node: Node): PlaylistTrack = node.attribute("Key") match { 11 | case Some(trackId) => 12 | PlaylistTrack(trackId.text) 13 | case _ => 14 | throw new IllegalArgumentException(s"Couldn't read a playlist track's XML, it was missing the 'Key' attribute: $node") 15 | } 16 | } 17 | 18 | object PlaylistTracks { 19 | def fromXml(playlistNode: Node): Seq[PlaylistTrack] = playlistNode \ "TRACK" map { PlaylistTrack.apply } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/repair/RekordBoxRewriteRule.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox.repair 2 | 3 | import com.vividlab.rekordbox.analyse.{LocateFileResult, LocateFileResults, Relocated} 4 | import com.vividlab.rekordbox.data.Playlist 5 | import com.vividlab.rekordbox.{Config, FileUtils} 6 | 7 | import scala.language.{implicitConversions, reflectiveCalls} 8 | import scala.xml._ 9 | import scala.xml.transform.RewriteRule 10 | 11 | /** 12 | * Rewrites the rekordbox XML file to the configured output file, modifying the locations of any files that have been 13 | * relocated, and limiting the output to only the repaired tracks and their related playlists if configured to do so. 14 | * 15 | * There are some minor issues with Scala's XML library worth mentioning: 16 | * 17 | * - When re-writing XML attributes, there is a known bug where the ordering of the attributes gets reversed, so for 18 | * example a collection track's TrackID attribute ends up being the last attribute at the end, rather than the first. 19 | * While the XML is still valid and will be read by rekordbox without issue, it's harder to work with as a developer 20 | * when you need to review the before-after delta. There is already a fix for this merged into the master branch of 21 | * the scala-xml project: https://github.com/scala/scala-xml/pull/172/files 22 | * 23 | * - Support for rewriting XML attributes could also be improved in the Scala XML library by providing better iteration 24 | * over MetaData lists and a better Attribute.copy function. Given those points, note the helper functions included 25 | * below. 26 | * 27 | * - The rewriter produces a lot of white space if sections of the input XML are being excluded from the output XML. 28 | * This doesn't seem to bother rekordbox, but it did create some issues for the unit tests, see the test 29 | * RelocatorTest... "End-to-end only outputting relocated tracks and their related playlists" for more info 30 | * 31 | * @param config Configuration specified by the user via the command line options 32 | * @param locateFileResults Results of the file relocate operation 33 | */ 34 | class RekordBoxRewriteRule( 35 | config: Config, 36 | locateFileResults: LocateFileResults 37 | ) extends RewriteRule { 38 | 39 | override def transform(node: Node): Seq[Node] = node match { 40 | case e: Elem if e.label == "COLLECTION" && config.outputRepairedTracksOnly => 41 | // Rewrite collection track count if we're only outputting relocated tracks 42 | rewriteAttribute(e, "Entries", locateFileResults.relocated.size.toString) 43 | 44 | case e: Elem if e.label == "TRACK" => 45 | 46 | def resultByTrackId(trackId: String): Option[LocateFileResult] = 47 | locateFileResults.results.find(_.track.id == trackId) 48 | 49 | // Match on either a collection track's TradeID attribute, or a playlist track's Key attribute 50 | (e.attribute("TrackID"), e.attribute("Key")) match { 51 | 52 | // Rewrite collection tracks with a new location attribute if necessary 53 | case (Some(trackId: Text), None) => 54 | resultByTrackId(trackId.text).map { 55 | case relocated: Relocated => 56 | rewriteAttribute(e, "Location", FileUtils.locationFromFile(relocated.newFile)) 57 | case _ => 58 | if (config.outputRepairedTracksOnly) 59 | Nil 60 | else 61 | node 62 | }.getOrElse(node) 63 | 64 | // Rewrite playlist tracks, limiting to only relocated tracks if requested 65 | case (None, Some(trackId)) if config.outputRepairedTracksOnly => 66 | resultByTrackId(trackId.text).map { 67 | case _: Relocated => node 68 | case _ => Nil 69 | }.getOrElse(node) 70 | 71 | case _ => node 72 | } 73 | 74 | // Filter playlists if we're only outputting relocated tracks down to only playlists containing relocated tracks 75 | case e: Elem if e.label == "NODE" && config.outputRepairedTracksOnly => 76 | if (locateFileResults.relocated.exists(result => Playlist(node).containsTrack(result.track.id))) 77 | node 78 | else 79 | Nil 80 | 81 | case _ => node 82 | } 83 | 84 | // The built-in copy function for attributes isn't sufficient so pimping it was necessary 85 | implicit def pimpedAttribute(attr: Attribute) = new { 86 | def pimpedCopy(key: String = attr.key, value: Any = attr.value): Attribute = 87 | Attribute(attr.pre, key, Text(value.toString), attr.next) 88 | } 89 | 90 | // Makes it possible to iterate over attributes using the preferred "for (attr <- e.attributes)" syntax 91 | implicit def pimpedIterableToMetaData(items: Iterable[MetaData]): MetaData = { 92 | items match { 93 | case Nil => 94 | Null 95 | case head :: tail => 96 | head.copy(next = pimpedIterableToMetaData(tail)) 97 | } 98 | } 99 | 100 | private def rewriteAttribute(e: Elem, attributeName: String, newValue: String): Elem = { 101 | e.copy(attributes = e.attributes.map { 102 | case attr@Attribute(attrName, _, _) if attrName == attributeName => 103 | attr.pimpedCopy(value = newValue) 104 | case other => 105 | other 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/com/vividlab/rekordbox/repair/Repairer.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox.repair 2 | 3 | import com.vividlab.rekordbox.Config 4 | import com.vividlab.rekordbox.analyse.AnalyserResult 5 | import org.slf4j.LoggerFactory 6 | 7 | import scala.xml.XML 8 | import scala.xml.transform.RuleTransformer 9 | 10 | object Repairer { 11 | private val log = LoggerFactory.getLogger(getClass) 12 | 13 | def repair(config: Config, analyserResult: AnalyserResult): Unit = { 14 | log.info("Generating repaired rekordbox collection...") 15 | 16 | val inputXml = XML.loadFile(config.inputXmlFile) 17 | 18 | log.info(s"Transforming XML...") 19 | val rule = new RekordBoxRewriteRule(config, analyserResult.locateFileResults) 20 | val outputXml = new RuleTransformer(rule).transform(inputXml) 21 | 22 | log.info(s"Saving new XML to ${config.outputXmlFile.getAbsolutePath}...") 23 | XML.save(config.outputXmlFile.getAbsolutePath, outputXml.head, "UTF-8", xmlDecl = true) 24 | 25 | log.info("Finished generating repaired rekordbox collection") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/resources/report-templates/expected-report-macos.txt: -------------------------------------------------------------------------------- 1 | rekordbox Repair Report 2 | 3 | 4 | Summary 5 | 6 | Total tracks in collection: 9 7 | Tracks OK: 2 8 | Tracks repaired: 3 9 | Tracks with multiple matches: 1 10 | Tracks with missing files: 2 11 | Tracks with invalid paths: 1 12 | Tracks with path too long: 1 13 | Tracks on disk but not in rekordbox: 2 14 | 15 | 16 | 17 | Repaired tracks 18 | 19 | The original files for the following tracks were not found in their expected locations, but were found elsewhere and repaired versions have been written to the output XML file: 20 | 21 | No Artist - No Album - 'HORN' 22 | Old: {testLibraryDirectory}/PioneerDJ/Sampler/OSC_SAMPLER/PRESET ONESHOT/HORN.wav 23 | New: {testLibraryDirectory}/PioneerDJ/Sampler/OSC_SAMPLER/PRESET RELOCATED/HORN.wav 24 | 25 | No Artist - No Album - 'Hash Track' 26 | Old: {testLibraryDirectory}/Name with #.mp3 27 | New: {testLibraryDirectory}/Special Filenames/Name with #.mp3 28 | 29 | 'Loopmasters' - No Album - 'Demo Track 2' 30 | Old: {testLibraryDirectory}/PioneerDJ/Demo Tracks/Demo Track 2.mp3 31 | New: {testLibraryDirectory}/PioneerDJ/Recording/Demo Track 2.mp3 32 | 33 | 34 | 35 | Tracks with multiple matches 36 | 37 | The original files for the following tracks were not found in their expected locations, but multiple files with the same name were found elsewhere: 38 | 39 | No Artist - No Album - 'SINEWAVE' 40 | Old: 41 | {testLibraryDirectory}/PioneerDJ/Sampler/OSC_SAMPLER/PRESET ONESHOT/SINEWAVE.wav 42 | 43 | New (Potentials): 44 | {testLibraryDirectory}/PioneerDJ/Sampler/OSC_SAMPLER/PRESET RELOCATED DUPLICATE 1/SINEWAVE.wav 45 | {testLibraryDirectory}/PioneerDJ/Sampler/OSC_SAMPLER/PRESET RELOCATED DUPLICATE 2/SINEWAVE.wav 46 | 47 | 48 | Tracks with missing files 49 | 50 | The files for the following tracks couldn't be found anywhere in the specified search directory: 51 | 52 | No Artist - No Album - 'NOISE' 53 | Missing: {testLibraryDirectory}/PioneerDJ/Sampler/OSC_SAMPLER/PRESET ONESHOT/NOISE.wav 54 | 55 | No Artist - No Album - 'SoundCloud Plugin Track' 56 | Missing: /soundcloud:tracks:1149223021 57 | 58 | 59 | 60 | Tracks with invalid locations 61 | 62 | The following tracks couldn't be analysed or repaired because the file locations specified in the rekordbox XML library file were in some way invalid, e.g. contained a raw question mark. For these files the fix is to remove them from your rekordbox collection then re-import them: 63 | 64 | No Artist - No Album - 'Question Mark Track' 65 | Invalid: file://localhost/Name%20with%20?.mp3 66 | 67 | 68 | 69 | Tracks with path too long 70 | 71 | rekordbox has a limit of 255 characters for the full path to a track's file, e.g. "/Users/User/Music/Library/Artist/Album/Music File.mp3" is 53 characters long. The following files exceed that limit and therefore can't be imported to rekordbox: 72 | 73 | {testLibraryDirectory}/File with a really really really really really really really really really really really really really really really really really really really really really really really long name.mp3 74 | 75 | 76 | Tracks on disk but not in rekordbox 77 | 78 | The following files exist in the specified search directory but haven't yet been imported into rekordbox: 79 | 80 | {testLibraryDirectory}/File with a really really really really really really really really really really really really really really really really really really really really really really really long name.mp3 81 | {testLibraryDirectory}/Not in rekordbox/Not in rekordbox.mp3 82 | -------------------------------------------------------------------------------- /src/test/resources/report-templates/expected-report-windows.txt: -------------------------------------------------------------------------------- 1 | rekordbox Repair Report 2 | 3 | 4 | Summary 5 | 6 | Total tracks in collection: 9 7 | Tracks OK: 2 8 | Tracks repaired: 3 9 | Tracks with multiple matches: 1 10 | Tracks with missing files: 1 11 | Tracks with invalid paths: 2 12 | Tracks with path too long: 0 13 | Tracks on disk but not in rekordbox: 1 14 | 15 | 16 | 17 | Repaired tracks 18 | 19 | The original files for the following tracks were not found in their expected locations, but were found elsewhere and repaired versions have been written to the output XML file: 20 | 21 | No Artist - No Album - 'HORN' 22 | Old: {testLibraryDirectory}\PioneerDJ\Sampler\OSC_SAMPLER\PRESET ONESHOT\HORN.wav 23 | New: {testLibraryDirectory}\PioneerDJ\Sampler\OSC_SAMPLER\PRESET RELOCATED\HORN.wav 24 | 25 | No Artist - No Album - 'Hash Track' 26 | Old: {testLibraryDirectory}\Name with #.mp3 27 | New: {testLibraryDirectory}\Special Filenames\Name with #.mp3 28 | 29 | 'Loopmasters' - No Album - 'Demo Track 2' 30 | Old: {testLibraryDirectory}\PioneerDJ\Demo Tracks\Demo Track 2.mp3 31 | New: {testLibraryDirectory}\PioneerDJ\Recording\Demo Track 2.mp3 32 | 33 | 34 | 35 | Tracks with multiple matches 36 | 37 | The original files for the following tracks were not found in their expected locations, but multiple files with the same name were found elsewhere: 38 | 39 | No Artist - No Album - 'SINEWAVE' 40 | Old: 41 | {testLibraryDirectory}\PioneerDJ\Sampler\OSC_SAMPLER\PRESET ONESHOT\SINEWAVE.wav 42 | 43 | New (Potentials): 44 | {testLibraryDirectory}\PioneerDJ\Sampler\OSC_SAMPLER\PRESET RELOCATED DUPLICATE 1\SINEWAVE.wav 45 | {testLibraryDirectory}\PioneerDJ\Sampler\OSC_SAMPLER\PRESET RELOCATED DUPLICATE 2\SINEWAVE.wav 46 | 47 | 48 | Tracks with missing files 49 | 50 | The files for the following tracks couldn't be found anywhere in the specified search directory: 51 | 52 | No Artist - No Album - 'NOISE' 53 | Missing: {testLibraryDirectory}\PioneerDJ\Sampler\OSC_SAMPLER\PRESET ONESHOT\NOISE.wav 54 | 55 | 56 | 57 | Tracks with invalid locations 58 | 59 | The following tracks couldn't be analysed or repaired because the file locations specified in the rekordbox XML library file were in some way invalid, e.g. contained a raw question mark. For these files the fix is to remove them from your rekordbox collection then re-import them: 60 | 61 | No Artist - No Album - 'Question Mark Track' 62 | Invalid: file://localhost/Name%20with%20?.mp3 63 | 64 | No Artist - No Album - 'SoundCloud Plugin Track' 65 | Invalid: file://localhost/soundcloud:tracks:1149223021 66 | 67 | 68 | 69 | Tracks with path too long 70 | 71 | rekordbox has a limit of 255 characters for the full path to a track's file, e.g. "/Users/User/Music/Library/Artist/Album/Music File.mp3" is 53 characters long. The following files exceed that limit and therefore can't be imported to rekordbox: 72 | 73 | 74 | 75 | 76 | Tracks on disk but not in rekordbox 77 | 78 | The following files exist in the specified search directory but haven't yet been imported into rekordbox: 79 | 80 | {testLibraryDirectory}\Not in rekordbox\Not in rekordbox.mp3 81 | -------------------------------------------------------------------------------- /src/test/resources/test-library/Not in rekordbox/Not a Music File.txt: -------------------------------------------------------------------------------- 1 | Present to ensure when scanning for files which don't exist in rekordbox we're only including music files -------------------------------------------------------------------------------- /src/test/resources/test-library/Not in rekordbox/Not in rekordbox.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/src/test/resources/test-library/Not in rekordbox/Not in rekordbox.mp3 -------------------------------------------------------------------------------- /src/test/resources/test-library/PioneerDJ/Demo Tracks/Demo Track 1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/src/test/resources/test-library/PioneerDJ/Demo Tracks/Demo Track 1.mp3 -------------------------------------------------------------------------------- /src/test/resources/test-library/PioneerDJ/Recording/Demo Track 2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/src/test/resources/test-library/PioneerDJ/Recording/Demo Track 2.mp3 -------------------------------------------------------------------------------- /src/test/resources/test-library/PioneerDJ/Sampler/OSC_SAMPLER/PRESET ONESHOT/SIREN.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/src/test/resources/test-library/PioneerDJ/Sampler/OSC_SAMPLER/PRESET ONESHOT/SIREN.wav -------------------------------------------------------------------------------- /src/test/resources/test-library/PioneerDJ/Sampler/OSC_SAMPLER/PRESET RELOCATED DUPLICATE 1/SINEWAVE.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/src/test/resources/test-library/PioneerDJ/Sampler/OSC_SAMPLER/PRESET RELOCATED DUPLICATE 1/SINEWAVE.wav -------------------------------------------------------------------------------- /src/test/resources/test-library/PioneerDJ/Sampler/OSC_SAMPLER/PRESET RELOCATED DUPLICATE 2/SINEWAVE.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/src/test/resources/test-library/PioneerDJ/Sampler/OSC_SAMPLER/PRESET RELOCATED DUPLICATE 2/SINEWAVE.wav -------------------------------------------------------------------------------- /src/test/resources/test-library/PioneerDJ/Sampler/OSC_SAMPLER/PRESET RELOCATED/HORN.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/src/test/resources/test-library/PioneerDJ/Sampler/OSC_SAMPLER/PRESET RELOCATED/HORN.wav -------------------------------------------------------------------------------- /src/test/resources/test-library/Special Filenames/Name with #.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edkennard/rekordbox-repair/35763ca0a5b4ee2c775cd4a40bd8ac41718cf991/src/test/resources/test-library/Special Filenames/Name with #.mp3 -------------------------------------------------------------------------------- /src/test/resources/xml-templates/broken-library.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 24 | 30 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 61 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/test/resources/xml-templates/expected-fixed-library-repaired-only.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/test/resources/xml-templates/expected-fixed-library.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 24 | 30 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 61 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/test/resources/xml-templates/missing-attribute-library.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/test/scala/com/vividlab/rekordbox/ConfigTest.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox 2 | 3 | import org.scalatest.freespec.AnyFreeSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | import java.io.File 7 | 8 | 9 | class ConfigTest extends AnyFreeSpec with Matchers with TestData { 10 | 11 | "Valid arguments" in { 12 | val args: Array[String] = Array( 13 | "-i", brokenLibraryXmlFile.getAbsolutePath, 14 | "-o", fixedLibraryXmlFile.getAbsolutePath, 15 | "-s", testLibraryDir.getAbsolutePath 16 | ) 17 | val config = Config(args) 18 | config.inputXmlFile shouldBe brokenLibraryXmlFile 19 | config.outputXmlFile shouldBe fixedLibraryXmlFile 20 | config.searchDirectory shouldBe testLibraryDir 21 | config.outputRepairedTracksOnly shouldBe true 22 | } 23 | 24 | "Valid arguments with optional 'output relocated tracks only' argument disabled" in { 25 | val args: Array[String] = Array( 26 | "-i", brokenLibraryXmlFile.getAbsolutePath, 27 | "-o", fixedLibraryXmlFile.getAbsolutePath, 28 | "-s", testLibraryDir.getAbsolutePath, 29 | "-r", "false" 30 | ) 31 | val config = Config(args) 32 | config.inputXmlFile shouldBe brokenLibraryXmlFile 33 | config.outputXmlFile shouldBe fixedLibraryXmlFile 34 | config.searchDirectory shouldBe testLibraryDir 35 | config.outputRepairedTracksOnly shouldBe false 36 | } 37 | 38 | "No arguments specified" in { 39 | val blankConfig = Config(None, None, None) 40 | 41 | assertThrows[IllegalStateException] { 42 | blankConfig.inputXmlFile 43 | } 44 | assertThrows[IllegalStateException] { 45 | blankConfig.outputXmlFile 46 | } 47 | assertThrows[IllegalStateException] { 48 | blankConfig.searchDirectory 49 | } 50 | } 51 | 52 | "Invalid arguments specified" in { 53 | assertThrows[IllegalStateException] { 54 | Config(Array[String]("blah", "blah")) 55 | } 56 | } 57 | 58 | "Invalid input XML file or search directory specified" in { 59 | val validXmlFile = brokenLibraryXmlFile 60 | val validDirectory = testLibraryDir 61 | val invalidXmlFile = new File("non-existent-file.xml") 62 | val invalidDirectory = new File("/NonExistentDir") 63 | 64 | assertThrows[IllegalArgumentException] { 65 | Config(Array( 66 | "-i", invalidXmlFile.getAbsolutePath, 67 | "-o", validXmlFile.getAbsolutePath, 68 | "-s", validDirectory.getAbsolutePath 69 | )) 70 | } 71 | 72 | assertThrows[IllegalArgumentException] { 73 | Config(Array( 74 | "-i", validXmlFile.getAbsolutePath, 75 | "-o", validXmlFile.getAbsolutePath, 76 | "-s", invalidDirectory.getAbsolutePath 77 | )) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/scala/com/vividlab/rekordbox/TestData.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox 2 | 3 | import java.io.File 4 | import java.nio.file.Files 5 | 6 | import scala.io.Source 7 | import scala.xml.XML 8 | 9 | trait TestData { 10 | 11 | private val tempDir = Files.createTempDirectory("rekordbox-repair") 12 | tempDir.toFile.deleteOnExit() 13 | 14 | private def tempFile(filename: String) = { 15 | val file = new File(s"$tempDir/$filename") 16 | file.deleteOnExit() 17 | file 18 | } 19 | 20 | // Library containing some sample files, some of which are based on the factory set of tracks loaded into 21 | // rekordbox after first installing it 22 | private val testLibraryDirPath: String = getClass.getResource("/test-library").getFile 23 | val testLibraryDir: File = new File(testLibraryDirPath) 24 | 25 | // Test XML files 26 | val brokenLibraryXmlFile: File = xmlFromTemplate("broken-library.xml") 27 | val expectedFixedLibraryXmlFile: File = xmlFromTemplate("expected-fixed-library.xml") 28 | val expectedFixedLibraryRepairedOnlyXmlFile: File = xmlFromTemplate("expected-fixed-library-repaired-only.xml") 29 | val missingAttributeLibraryXmlFile: File = xmlFromTemplate("missing-attribute-library.xml") 30 | val fixedLibraryXmlFile: File = tempFile("fixed-library.xml") 31 | 32 | // Test report files 33 | val fixedLibraryReportFile: File = FileUtils.reportFileFromXmlFile(fixedLibraryXmlFile) 34 | fixedLibraryReportFile.deleteOnExit() 35 | val expectedReportFile: File = reportFromTemplate(if (OS.isWindows) "expected-report-windows.txt" else "expected-report-macos.txt") 36 | 37 | // Create file with a long name only for testing in MacOS. If we have a file like this permanently in test-resources, 38 | // Windows will fail to download it when pulling the repo from Git. Windows has a limit for the full path of a file of 39 | // around 260 characters, which is probably why rekordbox limit it as well, to ensure collections are portable between 40 | // MacOS and Windows 41 | if (OS.isMac) { 42 | val file = new File(s"$testLibraryDir/File with a really really really really really really really really really really really really really really really really really really really really really really really long name.mp3") 43 | file.createNewFile() 44 | } 45 | 46 | /** 47 | * Generates a test XML file from the given template, with all track file locations updated to point to 48 | * the 'test-classes/test-library' resource dir where the sample music files have been copied, which will have a 49 | * different path depending on each machine the tests are run on. 50 | * 51 | * @param filename Name of the template XML file to open and transform, as well as the generated XML file to write 52 | * @return A reference to the generated temporary XML file 53 | */ 54 | private def xmlFromTemplate(filename: String): File = { 55 | val xmlLines = Source.fromResource(s"xml-templates/$filename").getLines() 56 | val xmlLinesCorrectPaths = xmlLines.map(_.replace("{testLibraryDirectory}", testLibraryDirPath)) 57 | val xml = XML.loadString(xmlLinesCorrectPaths.mkString(OS.newLine)) 58 | 59 | val xmlFile = tempFile(filename) 60 | XML.save(xmlFile.getAbsolutePath, xml.head, "UTF-8", xmlDecl = true) 61 | xmlFile 62 | } 63 | 64 | private def reportFromTemplate(filename: String): File = { 65 | val reportLines = Source.fromResource(s"report-templates/$filename").getLines() 66 | val reportLinesCorrectPaths = reportLines.map(_.replace("{testLibraryDirectory}", testLibraryDir.getCanonicalPath)) 67 | val report = reportLinesCorrectPaths.mkString(OS.newLine) 68 | 69 | val reportFile = tempFile(filename) 70 | FileUtils.writeTextFile(reportFile, report) 71 | reportFile 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/scala/com/vividlab/rekordbox/TestUtils.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox 2 | 3 | import java.io.File 4 | 5 | import scala.xml.{PrettyPrinter, XML} 6 | 7 | trait TestUtils { 8 | 9 | def xmlFilesMatch(file1: File, file2: File): Boolean = { 10 | val printer = new PrettyPrinter(200, 2) 11 | 12 | XML.loadString(printer.format(XML.loadFile(file1))) 13 | .equals(XML.loadString(printer.format(XML.loadFile(file2)))) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/scala/com/vividlab/rekordbox/analyse/AnalyserTest.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox.analyse 2 | 3 | import com.vividlab.rekordbox._ 4 | import org.scalatest.freespec.AnyFreeSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import org.slf4j.LoggerFactory 7 | 8 | import scala.io.Source 9 | 10 | class AnalyserTest extends AnyFreeSpec with Matchers with TestData with TestUtils { 11 | private val log = LoggerFactory.getLogger(getClass) 12 | 13 | "Analyse a broken rekordbox library" in { 14 | val config = Config(Array( 15 | "-i", brokenLibraryXmlFile.getAbsolutePath, 16 | "-o", fixedLibraryXmlFile.getAbsolutePath, 17 | "-s", testLibraryDir.getAbsolutePath, 18 | "-r", "false" 19 | )) 20 | 21 | Analyser.analyse(config) match { 22 | case Right(result) => 23 | 24 | val lr = result.locateFileResults 25 | lr.ok.size shouldBe 2 26 | lr.relocated.size shouldBe 3 27 | lr.multipleLocations.size shouldBe 1 28 | 29 | if (OS.isMac) { 30 | lr.missing.size shouldBe 2 31 | lr.invalid.size shouldBe 1 32 | result.filesNotInRekordBox.size shouldBe 2 33 | result.filesWithPathTooLong.size shouldBe 1 34 | } else { 35 | lr.missing.size shouldBe 1 36 | lr.invalid.size shouldBe 2 37 | result.filesNotInRekordBox.size shouldBe 1 38 | result.filesWithPathTooLong.size shouldBe 0 39 | } 40 | 41 | // Output summary to log just for developer's benefit when inspecting test output. The composition of the summary 42 | // text sent to the log here is verified below when testing the full report 43 | log.info(result.logText()) 44 | 45 | // Test the report's output 46 | FileUtils.writeReport(fixedLibraryReportFile, result) 47 | 48 | val expectedReportSource = Source.fromFile(expectedReportFile) 49 | val actualReportSource = Source.fromFile(fixedLibraryReportFile) 50 | 51 | val expectedReportText = expectedReportSource.getLines().mkString(OS.newLine) 52 | val actualReportText = actualReportSource.getLines().mkString(OS.newLine) 53 | 54 | expectedReportSource.close() 55 | actualReportSource.close() 56 | 57 | actualReportText shouldBe expectedReportText 58 | 59 | case Left(ex) => 60 | fail(ex) 61 | } 62 | } 63 | 64 | "Reject invalid rekordbox XML" in { 65 | val config = Config(Array( 66 | "-i", missingAttributeLibraryXmlFile.getAbsolutePath, 67 | "-o", fixedLibraryXmlFile.getAbsolutePath, 68 | "-s", testLibraryDir.getAbsolutePath 69 | )) 70 | 71 | Analyser.analyse(config).isLeft shouldBe true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/scala/com/vividlab/rekordbox/repair/RepairerTest.scala: -------------------------------------------------------------------------------- 1 | package com.vividlab.rekordbox.repair 2 | 3 | import com.vividlab.rekordbox.analyse.Analyser 4 | import com.vividlab.rekordbox.{Config, TestData, TestUtils} 5 | import org.scalatest.freespec.AnyFreeSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | import scala.xml.XML 9 | 10 | class RepairerTest extends AnyFreeSpec with Matchers with TestData with TestUtils { 11 | 12 | "Rewrite entire rekordbox XML file with repaired tracks" in { 13 | val config = Config(Array( 14 | "-i", brokenLibraryXmlFile.getAbsolutePath, 15 | "-o", fixedLibraryXmlFile.getAbsolutePath, 16 | "-s", testLibraryDir.getAbsolutePath, 17 | "-r", "false" 18 | )) 19 | 20 | Analyser.analyse(config) match { 21 | case Right(result) => 22 | Repairer.repair(config, result) 23 | XML.loadFile(fixedLibraryXmlFile) shouldBe XML.loadFile(expectedFixedLibraryXmlFile) 24 | case Left(ex) => 25 | fail(ex) 26 | } 27 | } 28 | 29 | "Write only repaired tracks and their related playlists to rekordbox XML file" in { 30 | val config = Config(Array( 31 | "-i", brokenLibraryXmlFile.getAbsolutePath, 32 | "-o", fixedLibraryXmlFile.getAbsolutePath, 33 | "-s", testLibraryDir.getAbsolutePath, 34 | "-r", "true" 35 | )) 36 | 37 | Analyser.analyse(config) match { 38 | case Right(result) => 39 | Repairer.repair(config, result) 40 | 41 | // This helper function is needed specifically for this test because comparing directly using XML.loadFile per 42 | // test "End-to-end" fails the test, even if the XML is good. It might be due to a lot of white space in the 43 | // transformed XML 44 | xmlFilesMatch(fixedLibraryXmlFile, expectedFixedLibraryRepairedOnlyXmlFile) shouldBe true 45 | 46 | case Left(ex) => 47 | fail(ex) 48 | } 49 | } 50 | } 51 | --------------------------------------------------------------------------------