diff --git a/.local/share/rhythmbox/plugins/fileorganizer/.gitignore b/.local/share/rhythmbox/plugins/fileorganizer/.gitignore new file mode 100644 index 0000000..fd57cd2 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/.gitignore @@ -0,0 +1,32 @@ +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + +#Geany +*.geany + +fo.conf diff --git a/.local/share/rhythmbox/plugins/fileorganizer/AUTHORS b/.local/share/rhythmbox/plugins/fileorganizer/AUTHORS new file mode 100644 index 0000000..dbc0fa9 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/AUTHORS @@ -0,0 +1,29 @@ +Wolter Hellmund + Original Author + Everything up to 1.0.1 & Revision 7 [1] + +Sharpeee [https://launchpad.net/~sharpeee] + Implemented database update upon file relocation. [2] + +Fayez [https://github.com/sirfz] + added strip_ntfs. [5] + +alzadude [https://github.com/alzadude] + General code fixes + Multiple library awareness [12] [13] + +Lachlan de Waard + 1.0.2 & Revision 8 onwards. [3] + GTK3 port and current code. [4] + Migrated to github [6] + + +[1] http://code.launchpad.net/~wolterh/rb-fileorganizer/main +[2] http://bugs.launchpad.net/rb-fileorganizer/+bug/575964 +[3] http://code.launchpad.net/~lachlan-00/rb-fileorganizer/legacy +[4] http://code.launchpad.net/~lachlan-00/rb-fileorganizer/trunk +[5] https://github.com/lachlan-00/rb-fileorganizer/pull/8 +[6] https://github.com/lachlan-00/rb-fileorganizer/ +[7] https://github.com/lachlan-00/rb-fileorganizer/pull/12 +[8] https://github.com/lachlan-00/rb-fileorganizer/pull/13 + diff --git a/.local/share/rhythmbox/plugins/fileorganizer/LICENSE b/.local/share/rhythmbox/plugins/fileorganizer/LICENSE new file mode 100644 index 0000000..2a9f5e8 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/LICENSE @@ -0,0 +1,5 @@ +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. + +http://creativecommons.org/licenses/by-sa/3.0/ diff --git a/.local/share/rhythmbox/plugins/fileorganizer/Makefile b/.local/share/rhythmbox/plugins/fileorganizer/Makefile new file mode 100644 index 0000000..db79746 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/Makefile @@ -0,0 +1,37 @@ +INSTALLPATH="$(HOME)/.local/share/rhythmbox/plugins/fileorganizer/" +INSTALLTEXT="The Fileorganizer plugin has been installed. You may now restart Rhythmbox and enable the 'Fileorganizer' plugin." +UNINSTALLTEXT="The Fileorganizer plugin had been removed. The next time you restart Rhythmbox it will dissappear from the plugins list." +PLUGINFILE="fileorganizer.plugin" + +install-req: + # Make environment + mkdir -p $(INSTALLPATH) + # Copy files, forcefully + cp $(PLUGINFILE) $(INSTALLPATH) -f + cp *.py $(INSTALLPATH) -f + cp config.ui $(INSTALLPATH) -f + cp fo.conf.template $(INSTALLPATH) -f + cp README.md $(INSTALLPATH) -f + cp LICENSE $(INSTALLPATH) -f + cp AUTHORS $(INSTALLPATH) -f + +install: install-req + @echo + @echo $(INSTALLTEXT) + +install-gui: install-req + # Notify graphically + zenity --info --title='Installation complete' --text=$(INSTALLTEXT) + +uninstall-req: + # Simply remove the installation path folder + rm -rf $(INSTALLPATH) + +uninstall: uninstall-req + @echo + @echo $(UNINSTALLTEXT) + +uninstall-gui: uninstall-req + # Notify graphically + zenity --info --title='Uninstall complete' --text=$(UNINSTALLTEXT) + diff --git a/.local/share/rhythmbox/plugins/fileorganizer/README.md b/.local/share/rhythmbox/plugins/fileorganizer/README.md new file mode 100644 index 0000000..1fe4736 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/README.md @@ -0,0 +1,236 @@ +Development Stop +================ + +Hi everyone, I took over this plugin many years ago and have since moved on to other methods of maintaining my library. If someone wants to fork it and take over i'm happy to let this go as i don't have the need for this plugin anymore. + + +RHYTHMBOX FILEORGANIZER +======================= + +Please help with testing this new release! +A lot of big changes have happened that need testing before i can be comfortable with a stable release. + +------------------------------------- +WARNING, ONGOING DEVELOPMENT VERSION. +------------------------------------- + + * Please be aware that for the moment this repo may have bugs + that i haven't noticed in my testing. + * I have tested all current features and they work as expected + (But that isn't a promise it will be stable for you) + +Welcome to version 3.91-dev + +This update removes a lot of code that doesn't have any real purpose in the current rhythmbox. + +I have dropped dbops.py and simplified the database naming using urllib.parse. + +We no longer look for cover art as this has changed from older versions of rhythmbox. + +Instead of updating tags this feature is removed. + +So far in testing the changes are setting correct paths but the files are sometimes becoming 'missing' +The files move and update but I think this may be due to my large library (180,000) +and that testing has been done over sshfs as well as local files. + + + + +1.0 Install +2.0 Usage & Main Features +2.1 Other Features +3.0 Configuration and customisation +3.1 Compilation Support +3.2 Plugin Preferences Window +4.0 Change History +5.0 Contribute +6.0 Links + +1.0 INSTALL +----------- + +To install from the terminal using make: +make install + +To check the dependencies, then install using python: +python3 ./install.py + +If you want to install manually, extract to the following directory: + * $HOME/.local/share/rhythmbox/plugins/fileorganizer/ + +You can test python dependencies by running: +python3 -c "import depends_test; depends_test.check()" + +Possible extra requirements are: + * python-configparser (I have to confirm this but i think it's a default module in python 3.2+) + * gir1.2-notify-0.7 (Debian name, GObject notify library) + * dconf-editor (to make changes to the rhythmbox library settings) + + +2.0 USAGE & MAIN FEATURES +------------------------- + +This plugin is pretty simple but it has a few complicated features under the hood. + +Once the plugin is installed, simply enable it in Rhythmbox. A restart of rhythmbox will be required to detect the plugin if it was open when you installed. + +When the plugin is enabled, you will notice an option in the right-click menu of music items (like songs) that will read 'Organize selection'. Clicking this will organize the selected files following a defined structure (see 3. Configuration and customisation) for both folders and filenames. That's all there is to it. + + +2.1 OTHER FEATURES + +Intelligent duplicate backup: + * When two songs have the same name, the plugin moves the file to a backup directory. + * If you lose a file, you'll probably in a folder named 'backup' in the root of your music library. + +Move all non music files with your music: + * When enabled, Fileorganizer will move files like text files and pictures with that music file. + * This is great for keeping all files organised, not just music. + + +Log file for all actions: + * The log file is an invaluable tool to see what happens when running fileorganizer. + * By default this file is hidden in your home folder: $HOME/.fileorganizer.log + + +3.0 CONFIGURATION AND CUSTOMISATION +----------------------------------- + +The output when running 'Organize Selection' is set from dconf-editor using default Rhythmbox settings: + * org.gnome.rhythmbox.library/layout-filename (Is the filename for your output) + * org.gnome.rhythmbox.library/layout-path (Is the folder path for your output) + * org.gnome.rhythmbox.rhythmdb/locations (Is your library path) + +Using these, your final output becomes: + * library + layout-path + layout-filename + +The Locations setting can actually be multiple locations, the first value is always taken by the plugin. + +The Variables for layout_path and layout_filename follow the same values as rhythmbox: + * %at -- album title + * %aa -- album artist (Album artist will use track artist if it does not exist) + * %aA -- album artist (lowercase) + * %as -- album artist sortname + * %aS -- album artist sortname (lowercase) + * %ay -- album release year + * %an -- album disc number + * %aN -- album disc number, zero padded + * %ag -- album genre + * %aG -- album genre (lowercase) + * %tn -- track number (i.e 8) + * %tN -- track number, zero padded (i.e 08) + * %tt -- track title + * %ta -- track artist + * %tA -- track artist (lowercase) + +Variables not ported yet: + * %ts -- track artist sortname + * %tS -- track artist sortname (lowercase) + + +3.1 COMPILATION SUPPORT + +Fileorganizer will use the album artist tag which is a part of rhythmbox and replace the artist field. For example: + * Path: /music/$artist/$year $album/$disc-$track - $title + * Input: /music/new/spawn soundtrack/01 - filter & the crystal method - trip like i do.mp3 + * Set Album Artist to 'Various' in Rhythmbox. + * Output: /music/Various/1997 Spawn/1-01 - Can't You (Trip Like I Do).mp3 + + +3.2 PLUGIN PREFERENCES WINDOW + +The preferences window gives you the ability to switch features on or off. + +Preview Mode + * If enabled, 'Organize Selection' will only check for changes and open a text report after completion. + +File/Folder Cleanup + * If enabled, files within the same folder that aren't music files are moved as well + +Remove Empty Folders + * If the source folder is empty after moving, delete the folder + +Log File: + * Set the filename of the log file (the base path is your home folder) + +Strip NTFS Chars + * Strip out characters that Windows can't handle. + (NTFS actually supports more characters than Windows allows) + + + +4.0 CHANGE HISTORY +------------------ + +3.99*-dev-* + * Removed tag update options and code + * Removed cover art import, the naming/format has changed + * Using urllib.parse to encode DB imports + +3.*-dev-* + * Added python script install.py to check all imports. + (Also added uninstall.py) + * Removed older v2.99 zip file + * Removed INSTALL & UNINSTALL (these were just calls to make anyway) + * Ongoing pylint/refactor changes. + * Update config window to remove depreciated widgets. (requires GTK+ 3.0) + * Move conf template into base plugin dir + +Update 2015/05/05: + * added strip_ntfs option (Care of @sirfz) + [https://github.com/lachlan-00/rb-fileorganizer/commit/d8cf611f969a1fc250e7348b4e53285d13f950f3] + +3.2013.09.16: +Currently running on RB 3.0 + * Tag Library python-eyed3 not available for python 3. + +2.0.1-2 features include: +Preview Mode + * Files are not moved or changed in any way while in preview mode. + * When completed up to two text files will open showing changes or possibly damaged files. + * To enable preview mode, set enable it in the preferences window. +Update Tags After Relocation + * The plugin now uses python-eyeD3 for checking tag values. + * After organising the selected files, fileorganizer will update the mp3 tags for you to + +2.0 features include: + * GTK3 Rhythmbox 3/GIT support + * Moved settings from Gconf to Gsettings + * Random bug fixes + * New code base [1] + +1.1 features include: + * UI Implemented + * Configuration File + * Import cover art from the source folder to the RB cache if found. + * Ability to disable file/folder cleanup and other features. + +1.0.3-2 features include: + * Fixes to backup support. + * UTF-8 encoding support. + * Fixed move folder contents with files. + * Notification on completion using pynotify. + * More code cleanup and additions. + +1.0.3 features include: + * File management of non music files. + * A physical log file stored in the home folder. + * Moved the backup folder to the root of the music library. + * Compilation support using rhythmbox's album artist field. + +1.0.2 features include: + * Support for Rhythmbox > 0.13.1 + * Added $disc and $year support. + + +5.0 CONTRIBUTE +-------------- + +To contribute, please refer to our github page [2] + + +6.0 LINKS +--------- + +[1] http://code.launchpad.net/~lachlan-00/rb-fileorganizer/legacy +[2] https://github.com/lachlan-00/rb-fileorganizer diff --git a/.local/share/rhythmbox/plugins/fileorganizer/TODO b/.local/share/rhythmbox/plugins/fileorganizer/TODO new file mode 100644 index 0000000..9049983 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/TODO @@ -0,0 +1 @@ +testing plan diff --git a/.local/share/rhythmbox/plugins/fileorganizer/config.ui b/.local/share/rhythmbox/plugins/fileorganizer/config.ui new file mode 100644 index 0000000..0a6510e --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/config.ui @@ -0,0 +1,197 @@ + + + + + + True + False + vertical + 2 + + + True + False + center + 7 + File Organizer Preferences + + + + + + + False + True + 0 + + + + + True + False + vertical + 5 + + + True + False + vertical + 5 + + + Preview Mode + True + True + False + start + right + True + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + vertical + 5 + + + File/Folder Cleanup + True + True + False + start + right + True + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + vertical + 5 + + + Remove Empty Folders + True + True + False + start + right + True + + + False + True + 1 + + + + + False + True + 2 + + + + + True + False + vertical + 5 + + + Strip NTFS Chars + True + True + False + start + right + True + + + False + True + 1 + + + + + False + True + 3 + + + + + True + False + 65 + + + Log File + True + True + False + right + True + + + False + True + 0 + + + + + True + True + + + + True + True + 1 + + + + + False + True + 4 + + + + + False + True + 1 + + + + diff --git a/.local/share/rhythmbox/plugins/fileorganizer/configurator.py b/.local/share/rhythmbox/plugins/fileorganizer/configurator.py new file mode 100644 index 0000000..88abd9d --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/configurator.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +""" Configuration (gsettings) handler for Fileorganizer + + ----------------Authors---------------- + Lachlan de Waard + Wolter Hellmund + ----------------Licence---------------- + Creative Commons - Attribution Share Alike v3.0 + +""" + + +from gi.repository import Gio + +# gsettings locations for library and output paths +RHYTHMBOX_RHYTHMDB = 'locations' +RHYTHMBOX_LIBRARY = {'layout-path', 'layout-filename'} + + +class FileorganizerConf(object): + """ Class to read RB values using dconf/gsettings """ + def __init__(self): + self.rhythmdbsettings = Gio.Settings("org.gnome.rhythmbox.rhythmdb") + self.librarysettings = Gio.Settings("org.gnome.rhythmbox.library") + + # Request value + def get_val(self, key): + """ Fill values according to the current value in gsettings """ + keypath = None + if key == RHYTHMBOX_RHYTHMDB: + return self.rhythmdbsettings[key] + elif key in RHYTHMBOX_LIBRARY: + return self.librarysettings[key] + else: + print('Invalid key requested') + return keypath diff --git a/.local/share/rhythmbox/plugins/fileorganizer/depends_test.py b/.local/share/rhythmbox/plugins/fileorganizer/depends_test.py new file mode 100644 index 0000000..1ad9b29 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/depends_test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +""" Fileorganizer: test your dependencies + + ----------------Authors---------------- + Lachlan de Waard + ----------------Licence---------------- + Creative Commons - Attribution Share Alike v3.0 + +""" + + +def check(): + """ Importing all libraries used by FileOrganizer """ + clear = False + try: + import os + import codecs + import configparser + import shutil + import subprocess + import time + import gi + import urllib.parse + + gi.require_version('Peas', '1.0') + gi.require_version('PeasGtk', '1.0') + gi.require_version('Notify', '0.7') + gi.require_version('RB', '3.0') + + from gi.repository import GObject, Peas, PeasGtk, Gtk, Notify, Gio + from gi.repository import RB + + clear = True + except ImportError as errormsg: + print('\nDependency Problem\n\n' + str(errormsg)) + + if clear: + print('\nAll FileOrganizer dependencies are satisfied\n') + return True + else: + return False diff --git a/.local/share/rhythmbox/plugins/fileorganizer/fileops.py b/.local/share/rhythmbox/plugins/fileorganizer/fileops.py new file mode 100644 index 0000000..15c647e --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/fileops.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 + +""" Fileorganizer file operations + + ----------------Authors---------------- + Lachlan de Waard + Wolter Hellmund + ----------------Licence---------------- + Creative Commons - Attribution Share Alike v3.0 + +""" + +import os +import shutil +import time +import configparser +import gi +import urllib.parse + +gi.require_version('RB', '3.0') + +from gi.repository import RB + +import tools + +from logops import LogFile + + +RB_METATYPES = ('at', 'aa', 'aA', 'as', 'aS', 'ay', 'an', 'aN', 'ag', 'aG', + 'tn', 'tN', 'tt', 'ta', 'tA') +RB_MEDIA_TYPES = ['.m4a', '.flac', '.ogg', '.mp2', '.mp3', '.wav', '.spx'] + +PROP = [RB.RhythmDBPropType.ALBUM, RB.RhythmDBPropType.ALBUM_ARTIST, + RB.RhythmDBPropType.ALBUM_ARTIST_FOLDED, + RB.RhythmDBPropType.ALBUM_ARTIST_SORTNAME, + RB.RhythmDBPropType.ALBUM_ARTIST_SORTNAME_FOLDED, + RB.RhythmDBPropType.YEAR, RB.RhythmDBPropType.DISC_NUMBER, + RB.RhythmDBPropType.GENRE, RB.RhythmDBPropType.GENRE_FOLDED, + RB.RhythmDBPropType.TRACK_NUMBER, RB.RhythmDBPropType.TITLE, + RB.RhythmDBPropType.ARTIST, RB.RhythmDBPropType.ARTIST_FOLDED] + +IN = ' IN: ' +OUT = ' OUT: ' +INFO = ' ** INFO: ' +ERROR = ' ** ERROR: ' +CONFLICT = ' ** CONFLICT: ' +NO_NEED = 'No need for file relocation' +STILL_MEDIA = 'Directory still contains media; keeping:' +FILE_EXISTS = 'File exists, directing to backup folder' +POSSIBLE_DAMAGE = "Source file damaged or missing tag information.\n" +DIR_REMOVED = 'Removing empty directory' +UPDATING = 'Updating Database:' + + +class MusicFile(object): + """ Class that performs all the file operations """ + + def __init__(self, fileorganizer, db_entry=None, strip_ntfs=False): + self.conf = configparser.RawConfigParser() + conffile = (os.getenv('HOME') + '/.local/share/rhythmbox/' + + 'plugins/fileorganizer/fo.conf') + self.conf.read(conffile) + self.rbfo = fileorganizer + self.rbdb = self.rbfo.rbdb + self.log = LogFile() + # self.url = UrlData() + self.strip_ntfs = strip_ntfs + if db_entry: + # Track and disc digits from gconf + padded = '%s' % ('%0' + str(2) + '.d') + single = '%s' % ('%0' + str(1) + '.d') + self.metadata = { + RB_METATYPES[0]: db_entry.get_string(PROP[0]), + RB_METATYPES[1]: db_entry.get_string(PROP[1]), + RB_METATYPES[2]: db_entry.get_string(PROP[2]), + RB_METATYPES[3]: db_entry.get_string(PROP[3]), + RB_METATYPES[4]: db_entry.get_string(PROP[4]), + RB_METATYPES[5]: str(db_entry.get_ulong(PROP[5])), + RB_METATYPES[6]: str(single % (db_entry.get_ulong(PROP[6]))), + RB_METATYPES[7]: str(padded % (db_entry.get_ulong(PROP[6]))), + RB_METATYPES[8]: db_entry.get_string(PROP[7]), + RB_METATYPES[9]: db_entry.get_string(PROP[8]), + RB_METATYPES[10]: str(single % (db_entry.get_ulong(PROP[9]))), + RB_METATYPES[11]: str(padded % (db_entry.get_ulong(PROP[9]))), + RB_METATYPES[12]: db_entry.get_string(PROP[10]), + RB_METATYPES[13]: db_entry.get_string(PROP[11]), + RB_METATYPES[14]: db_entry.get_string(PROP[12]) + } + self.location = db_entry.get_string(RB.RhythmDBPropType.LOCATION) + self.entry = db_entry + self.rbdb_rep = ('%28', '%29', '%2B', '%27', '%2C', '%3A', '%21', + '%24', '%26', '%2A', '%2C', '%2D', '%2E', '%3D', + '%40', '%5F', '%7E', '%C3%A8') + self.rbdb_itm = ('(', ')', '+', "'", ',', ':', '!', + '$', '&', '*', ',', '-', '.', '=', + '@', '_', '~', 'è') + + def set_ascii(self, string): + """ Change unicode codes back to ascii for RhythmDB + RythmDB doesn't use a full URL for file path + """ + count = 0 + while count < len(self.rbdb_rep): + string = string.replace(self.rbdb_rep[count], + self.rbdb_itm[count]) + count += 1 + + return string + + # Returns metadata of the music file + def get_metadata(self, key): + """ Return metadata of current file """ + for datum in self.metadata: + if key == datum: + return self.metadata[datum] + + # Non media clean up + def file_cleanup(self, source, destin): + """ Remove empty folders and move non-music files with selection """ + cleanup_enabled = self.conf.get('conf', 'cleanup_enabled') + remove_folders = self.conf.get('conf', 'cleanup_empty_folders') + if cleanup_enabled == 'True': + sourcedir = os.path.dirname(source) + destindir = os.path.dirname(destin) + foundmedia = False + # Remove empty folders, if any + if os.path.isdir(sourcedir): + if not os.listdir(sourcedir) == []: + for files in os.listdir(sourcedir): + filelist = files[(files.rfind('.')):] + if filelist in RB_MEDIA_TYPES or os.path.isdir( + sourcedir + '/' + files): + foundmedia = True + elif not destindir == sourcedir: + mvdest = destindir + '/' + os.path.basename(files) + mvsrc = sourcedir + '/' + os.path.basename(files) + try: + shutil.move(mvsrc, mvdest) + except FileNotFoundError: + self.log.log_processing(ERROR + 'Moving ' + + files) + except PermissionError: + self.log.log_processing(ERROR + 'Moving ' + + files) + except Exception as e: + self.log.log_processing(ERROR + 'Moving ' + + files) + print(e) + finally: + self.log.log_processing(INFO + 'Moved') + self.log.log_processing(' ' + mvdest) + if foundmedia: + self.log.log_processing(INFO + STILL_MEDIA) + # remove empty folders after moving additional files + if os.listdir(sourcedir) == [] and remove_folders == 'True': + currentdir = sourcedir + self.log.log_processing(INFO + DIR_REMOVED) + while not os.listdir(currentdir): + self.log.log_processing(' ' + currentdir) + os.rmdir(currentdir) + currentdir = os.path.split(currentdir)[0] + + # Get Source and Destination separately so preview can use the same code + def get_locations(self, inputstring): + """ Get file path for other file operations """ + # Get source for comparison + source = self.location.replace('file:///', '/') + if inputstring == 'source': + return urllib.parse.unquote(source) + # Set Destination Directory + targetdir = '/' + self.rbfo.configurator.get_val('layout-path') + targetdir = tools.data_filler(self, targetdir, + strip_ntfs=self.strip_ntfs) + targetloc = self.rbfo.configurator.get_val('locations')[0] + targetpath = targetloc.replace('file:///', '/') + targetdir = tools.folderize(targetpath, targetdir) + # Set Destination Filename + targetname = self.rbfo.configurator.get_val('layout-filename') + targetname = tools.data_filler(self, targetname, + strip_ntfs=self.strip_ntfs) + targetname += os.path.splitext(self.location)[1] + # Join destination + if inputstring == 'destin': + return urllib.parse.unquote((os.path.join(targetdir, targetname))) + return + + def preview(self): + """ Running in preview mode does not change files in any way """ + print('preview') + previewlist = os.getenv('HOME') + '/.fileorganizer-preview.log' + damagedlist = os.getenv('HOME') + '/.fileorganizer-damaged.log' + source = self.get_locations('source') + destin = urllib.parse.unquote(self.get_locations('destin')) + if not source == destin: + # Write to preview list + logfile = open(previewlist, "a") + logfile.write("Change Found:\n" + source + "\n") + logfile.write(destin + "\n\n") + logfile.close() + + # Moves the file to a specific location with a specific name + def relocate(self): + """Performs the actual moving. + -Move file to correct place + -Update file location in RB database. + """ + source = self.get_locations('source') + destin = urllib.parse.unquote(self.get_locations('destin')) + # Begin Log File + tmptime = time.strftime("%I:%M:%S %p", time.localtime()) + logheader = '%ta - %at - ' + logheader = (tools.data_filler(self, logheader, + strip_ntfs=self.strip_ntfs) + tmptime) + # self.log = LogFile() + self.log.log_processing(logheader) + self.log.log_processing((IN + source)) + + # Relocate, if necessary + if source == destin: + print('No need for file relocation') + self.log.log_processing(INFO + NO_NEED) + else: + if os.path.isfile(destin): + # Copy the existing file to a backup dir + tmpdir = (self.rbfo.configurator.get_val('locations'))[0].replace('file:///', '/') + tmpdir = urllib.parse.unquote(tmpdir) + backupdir = tools.folderize(tmpdir, 'backup/') + backup = os.path.join(backupdir, os.path.basename(destin)) + if os.path.isfile(backup): + counter = 0 + backuptest = backup + while os.path.isfile(backup): + backup = backuptest + counter += 1 + backup = (backup[:(backup.rfind('.'))] + str(counter) + + backup[(backup.rfind('.')):]) + try: + os.makedirs(os.path.dirname(backupdir)) + except OSError: + pass + try: + shutil.move(source, backup) + self.log.log_processing(CONFLICT + FILE_EXISTS) + self.log.log_processing(OUT + backup) + except FileNotFoundError: + # we found a duplicate in the DB + pass + destin = backup + else: + # Move the file to desired destination + shutil.move(source, destin) + self.log.log_processing(OUT + destin) + + # Update Rhythmbox database + self.location = urllib.parse.quote(destin) + self.location = ('file://' + self.location) + self.location = self.set_ascii(self.location) + print('Relocating file \n%s to\n%s' % (source, destin)) + self.log.log_processing(INFO + UPDATING) + print(self.entry.get_string(RB.RhythmDBPropType.LOCATION)) + print(self.location) + self.log.log_processing(IN + self.entry.get_string(RB.RhythmDBPropType.LOCATION)) + self.log.log_processing(OUT + self.location) + # Make the change + self.rbdb.entry_set(self.entry, + RB.RhythmDBPropType.LOCATION, + self.location) + # Non media clean up + self.file_cleanup(source, destin) + self.log.log_processing('') diff --git a/.local/share/rhythmbox/plugins/fileorganizer/fileorganizer.plugin b/.local/share/rhythmbox/plugins/fileorganizer/fileorganizer.plugin new file mode 100644 index 0000000..c1e8650 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/fileorganizer.plugin @@ -0,0 +1,10 @@ +[Plugin] +Loader=python3 +Module=fileorganizer +IAge=2 +Depends=rb +Name=File Organizer +Description=A music file and folder organizer +Authors=Lachlan de Waard , Wolter Hellmund +Copyright=Copyright © 2010 Wolter Hellmund +Website=https://github.com/lachlan-00/rb-fileorganizer diff --git a/.local/share/rhythmbox/plugins/fileorganizer/fileorganizer.py b/.local/share/rhythmbox/plugins/fileorganizer/fileorganizer.py new file mode 100644 index 0000000..5463438 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/fileorganizer.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 + +""" Fileorganizer + + ----------------Authors---------------- + Lachlan de Waard + Wolter Hellmund + ----------------Licence---------------- + Creative Commons - Attribution Share Alike v3.0 + +""" + +import configparser +import os +import shutil +import gi + +gi.require_version('Peas', '1.0') +gi.require_version('PeasGtk', '1.0') +gi.require_version('Notify', '0.7') +gi.require_version('RB', '3.0') + +from gi.repository import GObject, Peas, PeasGtk, Gtk, Notify, Gio +from gi.repository import RB + +import fileops +import tools + +from configurator import FileorganizerConf + +PLUGIN_PATH = 'plugins/fileorganizer/' +CONFIGFILE = 'fo.conf' +CONFIGTEMPLATE = 'fo.conf.template' +UIFILE = 'config.ui' +C = "conf" + + +class Fileorganizer(GObject.Object, Peas.Activatable, PeasGtk.Configurable): + """ Main class that loads fileorganizer into Rhythmbox """ + __gtype_name = 'fileorganizer' + object = GObject.property(type=GObject.Object) + _menu_names = ['browser-popup', + 'playlist-popup'] + + def __init__(self, *args, **kwargs): + GObject.Object.__init__(self) + super(Fileorganizer, self).__init__(*args, **kwargs) + self.configurator = FileorganizerConf() + self.conf = configparser.RawConfigParser() + self.configfile = RB.find_user_data_file(PLUGIN_PATH + CONFIGFILE) + self.ui_file = RB.find_user_data_file(PLUGIN_PATH + UIFILE) + self.shell = None + self.rbdb = None + self.action_group = None + self.action = None + self.source = None + self.plugin_info = "fileorganizer" + + # Rhythmbox standard Activate method + def do_activate(self): + """ Activate the plugin """ + print("activating Fileorganizer") + shell = self.object + self.shell = shell + self.rbdb = shell.props.db + self._check_configfile() + self.menu_build(shell) + + # Rhythmbox standard Deactivate method + def do_deactivate(self): + """ Deactivate the plugin """ + print("deactivating Fileorganizer") + app = Gio.Application.get_default() + for menu_name in Fileorganizer._menu_names: + app.remove_plugin_menu_item(menu_name, 'selection-' + 'organize') + self.action_group = None + self.action = None + # self.source.delete_thyself() + self.source = None + + # FUNCTIONS + # check if configfile is present, if not copy from template folder + def _check_configfile(self): + """ Copy the default config template or load existing config file """ + if not os.path.isfile(self.configfile): + template = RB.find_user_data_file(PLUGIN_PATH + CONFIGTEMPLATE) + folder = os.path.split(self.configfile)[0] + if not os.path.exists(folder): + os.makedirs(folder) + shutil.copyfile(template, self.configfile) + + # Build menu option + def menu_build(self, shell): + """ Add 'Organize Selection' to the Rhythmbox righ-click menu """ + app = Gio.Application.get_default() + + # create action + action = Gio.SimpleAction(name="organize-selection") + action.connect("activate", self.organize_selection) + app.add_action(action) + + # create menu item + item = Gio.MenuItem() + item.set_label("Organize Selection") + item.set_detailed_action("app.organize-selection") + + # add plugin menu item + # app.add_plugin_menu_item('browser-popup', "Organize Selection", item) + for menu_name in Fileorganizer._menu_names: + app.add_plugin_menu_item(menu_name, "Organize Selection", item) + app.add_action(action) + + # Create the Configure window in the rhythmbox plugins menu + def do_create_configure_widget(self): + """ Load the glade UI for the config window """ + build = Gtk.Builder() + build.add_from_file(self.ui_file) + self._check_configfile() + self.conf.read(self.configfile) + window = build.get_object("fileorganizer") + build.get_object("log_path").set_text(self.conf.get(C, "log_path")) + if self.conf.get(C, "log_enabled") == "True": + build.get_object("logbutton").set_active(True) + if self.conf.get(C, "cleanup_enabled") == "True": + build.get_object("cleanupbutton").set_active(True) + if self.conf.get(C, "cleanup_empty_folders") == "True": + build.get_object("removebutton").set_active(True) + if self.conf.get(C, "preview_mode") == "True": + build.get_object("previewbutton").set_active(True) + if self.conf.get(C, "strip_ntfs") == "True": + build.get_object("ntfsbutton").set_active(True) + + build.get_object("logbutton").connect('clicked', lambda x: self.save_config(build)) + build.get_object("log_path").connect('changed', lambda x: self.save_config(build)) + build.get_object("cleanupbutton").connect('clicked', lambda x: self.save_config(build)) + build.get_object("removebutton").connect('clicked', lambda x: self.save_config(build)) + build.get_object("previewbutton").connect('clicked', lambda x: self.save_config(build)) + build.get_object("ntfsbutton").connect('clicked', lambda x: self.save_config(build)) + + return window + + def save_config(self, builder): + """ Save changes to the plugin config """ + if builder.get_object("logbutton").get_active(): + self.conf.set(C, "log_enabled", "True") + else: + self.conf.set(C, "log_enabled", "False") + + if builder.get_object("cleanupbutton").get_active(): + self.conf.set(C, "cleanup_enabled", "True") + else: + self.conf.set(C, "cleanup_enabled", "False") + + if builder.get_object("removebutton").get_active(): + self.conf.set(C, "cleanup_empty_folders", "True") + else: + self.conf.set(C, "cleanup_empty_folders", "False") + if builder.get_object("previewbutton").get_active(): + self.conf.set(C, "preview_mode", "True") + else: + self.conf.set(C, "preview_mode", "False") + if builder.get_object("ntfsbutton").get_active(): + self.conf.set(C, "strip_ntfs", "True") + else: + self.conf.set(C, "strip_ntfs", "False") + self.conf.set(C, "log_path", + builder.get_object("log_path").get_text()) + datafile = open(self.configfile, "w") + self.conf.write(datafile) + datafile.close() + + # Organize selection + def organize_selection(self, action, shell): + """ get your current selection and run process_selection """ + page = self.shell.props.selected_page + if not hasattr(page, "get_entry_view"): + return + selected = page.get_entry_view() + selection = selected.get_selected_entries() + self.process_selection(selection) + + # Process selection: Run in Preview Mode or Normal Mode + def process_selection(self, filelist): + """ using your selection, run the preview or process from fileops """ + self.conf.read(self.configfile) + strip_ntfs = self.conf.get(C, "strip_ntfs") == "True" + # Run in Preview Modelogops + if self.conf.get(C, "preview_mode") == "True": + if filelist: + prelist = os.getenv('HOME') + '/.fileorganizer-preview.log' + datafile = open(prelist, "w") + datafile.close() + damlist = os.getenv('HOME') + '/.fileorganizer-damaged.log' + datafile = open(damlist, "w") + datafile.close() + for item in filelist: + item = fileops.MusicFile(self, item, strip_ntfs=strip_ntfs) + item.preview() + Notify.init('Fileorganizer') + title = 'Fileorganizer' + note = 'Preview Has Completed' + notification = Notify.Notification.new(title, note, None) + Notify.Notification.show(notification) + # Show Results of preview + tools.results(prelist, damlist) + else: + # Run Normally + self.organize(filelist, strip_ntfs) + Notify.init('Fileorganizer') + title = 'Fileorganizer' + note = 'Your selection is organised' + notification = Notify.Notification.new(title, note, None) + Notify.Notification.show(notification) + return + + # Organize array of files + def organize(self, filelist, strip_ntfs=False): + """ get fileops to move media files to the correct location """ + if filelist: + for item in filelist: + item = fileops.MusicFile(self, item, strip_ntfs=strip_ntfs) + item.relocate() + return + + +class PythonSource(RB.Source): + """ Register with rhythmbox """ + + def __init__(self): + RB.Source.__init__(self) + GObject.type_register_dynamic(PythonSource) diff --git a/.local/share/rhythmbox/plugins/fileorganizer/fo.conf.template b/.local/share/rhythmbox/plugins/fileorganizer/fo.conf.template new file mode 100644 index 0000000..b03aaad --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/fo.conf.template @@ -0,0 +1,8 @@ +[conf] +cleanup_empty_folders = True +cleanup_enabled = True +log_path = .fileorganizer.log +log_enabled = True +preview_mode = False +strip_ntfs = False + diff --git a/.local/share/rhythmbox/plugins/fileorganizer/install.py b/.local/share/rhythmbox/plugins/fileorganizer/install.py new file mode 100755 index 0000000..fbd8da8 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/install.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +""" FileOrganizer Safe Install Script + + Install if dependencies are satisfied + +""" + +import os +import shutil + +import depends_test + +INSTALLPATH = os.path.join(os.getenv('HOME'), + ".local/share/rhythmbox/plugins/fileorganizer") + +# The depends test will check for required modules +if depends_test.check(): + # check plugin directory + if not os.path.exists(INSTALLPATH): + os.makedirs(INSTALLPATH) + # copy the contents of the plugin directory + for i in os.listdir('./'): + if os.path.isfile(i): + print('Copying... ' + i) + shutil.copy(i, INSTALLPATH) + print('\nFileOrganizer is now installed\n') +else: + print('please check your OS for missing packages') diff --git a/.local/share/rhythmbox/plugins/fileorganizer/logops.py b/.local/share/rhythmbox/plugins/fileorganizer/logops.py new file mode 100644 index 0000000..644e9b4 --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/logops.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +""" Fileorganizer log operations + + ----------------Authors---------------- + Lachlan de Waard + ----------------Licence---------------- + Creative Commons - Attribution Share Alike v3.0 + +""" + + +import os +import codecs +import configparser + + +class LogFile(object): + """ Log file actions. Open, create and edit log files """ + def __init__(self): + self.conf = configparser.RawConfigParser() + conffile = (os.getenv('HOME') + '/.local/share/rhythmbox/' + + 'plugins/fileorganizer/fo.conf') + self.conf.read(conffile) + + # Write to log file + def log_processing(self, logmessage): + """ Perform log operations """ + log_enabled = self.conf.get('conf', 'log_enabled') + log_filename = self.conf.get('conf', 'log_path') + log_filename = os.getenv('HOME') + '/' + log_filename + # Log if Enabled + if log_enabled == 'True': + # Create if missing + if (not os.path.exists(log_filename) or + os.path.getsize(log_filename) >= 1076072): + files = codecs.open(log_filename, "w", "utf8") + files.close() + files = codecs.open(log_filename, "a", "utf8") + try: + logline = [logmessage] + files.write((u"".join(logline)) + u"\n") + except UnicodeDecodeError: + print('LOG UNICODE ERROR') + logline = [logmessage.decode('utf-8')] + files.write((u"".join(logline)) + u"\n") + files.close() diff --git a/.local/share/rhythmbox/plugins/fileorganizer/tools.py b/.local/share/rhythmbox/plugins/fileorganizer/tools.py new file mode 100644 index 0000000..4e36bdc --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/tools.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +""" Fileorganizer tools + + ----------------Authors---------------- + Lachlan de Waard + Wolter Hellmund + ----------------Licence---------------- + Creative Commons - Attribution Share Alike v3.0 + +""" + +import os +import subprocess + +import fileops + + +class LibraryLocationError(Exception): + """To be raised when a file:// library location could not be found""" + + +# Returns the library location for a file, +# or the default location if the file is not inside any library location +# Raises an error if there are no file:// locations in the library +def library_location(files, library_locations): + file_locations = list(l for l in library_locations if l.startswith('file://')) + if not file_locations: + raise LibraryLocationError('No file:// locations could be found in the library') + return next((l for l in file_locations if files.location.startswith(l)), + file_locations[0]) + + +# Create a folder inside a library path if non-existent, and return it +def folderize(library_path, folder): + """ Create folders for file operations """ + dirpath = library_path + '/' + # Strip full stops from paths + folder = folder.replace('/.', '/_') + if not os.path.exists(dirpath + folder): + os.makedirs(dirpath + folder) + return os.path.normpath(dirpath + folder) + + +# Replace the placeholders with the correct values +def data_filler(files, string, strip_ntfs=False): + """ replace string data with metadata from current item """ + string = str(string) + for key in fileops.RB_METATYPES: + if '%' + key in string: + if key == 'aa': + artisttest = files.get_metadata('aa') + if artisttest == '': + string = string.replace(('%' + key), + process(files.get_metadata('ta'), + strip_ntfs)) + # print(string + ' ALBUM ARTIST NOT FOUND') + else: + string = string.replace(('%' + key), + process(files.get_metadata(key), + strip_ntfs)) + # print(string + ' ALBUM ARTIST FOUND') + else: + string = string.replace(('%' + key), + process(files.get_metadata(key), + strip_ntfs)) + return string + + +# Process names and replace any undesired characters +def process(string, strip_ntfs=False): + """ Prevent / character to avoid creating folders """ + string = string.replace('/', '_') # if present + string = string.replace(' ', '_') + if strip_ntfs: + string = ''.join(c for c in string if c not in '<>:"\\|?*') + while string.endswith('.'): + string = string[:-1] + return string + + +def results(prelist, damlist): + """ Show the results of your preview run """ + if not os.stat(prelist)[6] == 0: + print('fileorganizer: open preview list') + subprocess.Popen(['/usr/bin/xdg-open', prelist]) + if not os.stat(damlist)[6] == 0: + print('fileorganizer: open damaged file list') + subprocess.Popen(['/usr/bin/xdg-open', damlist]) + return + diff --git a/.local/share/rhythmbox/plugins/fileorganizer/uninstall.py b/.local/share/rhythmbox/plugins/fileorganizer/uninstall.py new file mode 100644 index 0000000..8ba252c --- /dev/null +++ b/.local/share/rhythmbox/plugins/fileorganizer/uninstall.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +""" FileOrganizer Uninstall Script + + Remove files from the plugin folder + +""" + +import os +import shutil + +INSTALLPATH = os.path.join(os.getenv('HOME'), + ".local/share/rhythmbox/plugins/fileorganizer") +TEMPLATEPATH = os.path.join(INSTALLPATH, 'template') + +if os.path.isdir(INSTALLPATH): + shutil.rmtree(INSTALLPATH) + print('\nFileOrganizer is uninstalled\n') +else: + print('\nFileOrganizer is not installed\n')