oh yeah nice organized moosiic

This commit is contained in:
lickthecheese 2020-06-15 10:53:43 -04:00
parent 8154bda936
commit ca278203be
17 changed files with 1322 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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/

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1 @@
testing plan

View File

@ -0,0 +1,197 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.1 -->
<interface>
<requires lib="gtk+" version="3.10"/>
<object class="GtkBox" id="fileorganizer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="toplabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_bottom">7</property>
<property name="label" translatable="yes">File Organizer Preferences</property>
<attributes>
<attribute name="style" value="normal"/>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="settingsbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="previewbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkCheckButton" id="previewbutton">
<property name="label" translatable="yes">Preview Mode</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="image_position">right</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="cleanupbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkCheckButton" id="cleanupbutton">
<property name="label" translatable="yes">File/Folder Cleanup</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="image_position">right</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="removebox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkCheckButton" id="removebutton">
<property name="label" translatable="yes">Remove Empty Folders</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="image_position">right</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="ntfsbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkCheckButton" id="ntfsbutton">
<property name="label" translatable="yes">Strip NTFS Chars</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="image_position">right</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox" id="log_pathbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">65</property>
<child>
<object class="GtkCheckButton" id="logbutton">
<property name="label" translatable="yes">Log File</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="image_position">right</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="log_path">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">●</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
""" Configuration (gsettings) handler for Fileorganizer
----------------Authors----------------
Lachlan de Waard <lachlan.00@gmail.com>
Wolter Hellmund <wolterh6@gmail.com>
----------------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

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
""" Fileorganizer: test your dependencies
----------------Authors----------------
Lachlan de Waard <lachlan.00@gmail.com>
----------------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

View File

@ -0,0 +1,270 @@
#!/usr/bin/env python3
""" Fileorganizer file operations
----------------Authors----------------
Lachlan de Waard <lachlan.00@gmail.com>
Wolter Hellmund <wolterh6@gmail.com>
----------------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('')

View File

@ -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 <lachlan.00@gmail.com>, Wolter Hellmund <wolterh6@gmail.com>
Copyright=Copyright © 2010 Wolter Hellmund
Website=https://github.com/lachlan-00/rb-fileorganizer

View File

@ -0,0 +1,231 @@
#!/usr/bin/env python3
""" Fileorganizer
----------------Authors----------------
Lachlan de Waard <lachlan.00@gmail.com>
Wolter Hellmund <wolterh6@gmail.com>
----------------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)

View File

@ -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

View File

@ -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')

View File

@ -0,0 +1,47 @@
#!/usr/bin/env python3
""" Fileorganizer log operations
----------------Authors----------------
Lachlan de Waard <lachlan.00@gmail.com>
----------------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()

View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
""" Fileorganizer tools
----------------Authors----------------
Lachlan de Waard <lachlan.00@gmail.com>
Wolter Hellmund <wolterh6@gmail.com>
----------------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

View File

@ -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')