diff --git a/api/browse.py b/api/browse.py index f8ffd58..62bcaa4 100755 --- a/api/browse.py +++ b/api/browse.py @@ -2,7 +2,7 @@ from flask import request from web import app -from db import MusicFolder, Artist, Album, Track +from db import Folder, Artist, Album, Track import uuid, time, string @app.route('/rest/getMusicFolders.view') @@ -12,7 +12,7 @@ def list_folders(): 'musicFolder': [ { 'id': str(f.id), 'name': f.name - } for f in MusicFolder.query.order_by(MusicFolder.name).all() ] + } for f in Folder.query.filter(Folder.root == True).order_by(Folder.name).all() ] } }) @@ -27,16 +27,16 @@ def list_indexes(): return request.error_formatter(0, 'Invalid timestamp') if musicFolderId is None: - folder = MusicFolder.query.all() + folder = Folder.query.filter(Folder.root == True).all() else: try: mfid = uuid.UUID(musicFolderId) except: return request.error_formatter(0, 'Invalid id') - folder = MusicFolder.query.get(mfid) + folder = Folder.query.get(mfid) - if not folder: + if not folder or not folder.root: return request.error_formatter(70, 'Folder not found') last_modif = max(map(lambda f: f.last_scan, folder)) if type(folder) is list else folder.last_scan diff --git a/db.py b/db.py index c4f0a19..cccaa6e 100755 --- a/db.py +++ b/db.py @@ -55,14 +55,18 @@ class User(Base): salt = Column(String(6)) admin = Column(Boolean) -class MusicFolder(Base): +class Folder(Base): __tablename__ = 'folder' id = UUID.gen_id_column() - name = Column(String, unique = True) - path = Column(String) + root = Column(Boolean, default = False) + name = Column(String) + path = Column(String, unique = True) last_scan = Column(DateTime, default = datetime.datetime.min) + parent_id = Column(UUID, ForeignKey('folder.id'), nullable = True) + children = relationship('Folder', backref = backref('parent', remote_side = [ id ])) + class Artist(Base): __tablename__ = 'artist' @@ -92,13 +96,15 @@ class Track(Base): path = Column(String, unique = True) bitrate = Column(Integer) + root_folder_id = Column(UUID, ForeignKey('folder.id')) + root_folder = relationship('Folder', primaryjoin = Folder.id == root_folder_id) folder_id = Column(UUID, ForeignKey('folder.id')) - folder = relationship('MusicFolder') + folder = relationship('Folder', primaryjoin = Folder.id == folder_id, backref = 'tracks') def as_subsonic_child(self): info = { 'id': str(self.id), - 'parent': str(self.album.id), + 'parent': str(self.folder.id), 'isDir': False, 'title': self.title, 'album': self.album.name, @@ -109,7 +115,7 @@ class Track(Base): 'suffix': 'mp3', # same as above 'duration': self.duration, 'bitRate': self.bitrate, - 'path': self.path[len(self.folder.path) + 1:], + 'path': self.path[len(self.root_folder.path) + 1:], 'isVideo': False, 'discNumber': self.disc, 'albumId': str(self.album.id), diff --git a/scanner.py b/scanner.py index a521abe..186b405 100755 --- a/scanner.py +++ b/scanner.py @@ -1,6 +1,6 @@ # coding: utf-8 -import os.path +import os, os.path import datetime import eyeD3 @@ -10,6 +10,8 @@ class Scanner: def __init__(self, session): self.__session = session self.__artists = db.Artist.query.all() + self.__folders = db.Folder.query.all() + self.__added_artists = 0 self.__added_albums = 0 self.__added_tracks = 0 @@ -27,9 +29,10 @@ class Scanner: def prune(self, folder): for artist in db.Artist.query.all(): for album in artist.albums[:]: - for track in filter(lambda t: t.folder.id == folder.id, album.tracks): + for track in filter(lambda t: t.root_folder.id == folder.id, album.tracks): if not os.path.exists(track.path): album.tracks.remove(track) + track.folder.tracks.remove(track) self.__session.delete(track) self.__deleted_tracks += 1 if len(album.tracks) == 0: @@ -40,6 +43,8 @@ class Scanner: self.__session.delete(artist) self.__deleted_artists += 1 + self.__cleanup_folder(folder) + def __scan_file(self, path, folder): tag = eyeD3.Tag() tag.link(path) @@ -48,7 +53,7 @@ class Scanner: al = self.__find_album(tag.getArtist(), tag.getAlbum()) tr = filter(lambda t: t.path == path, al.tracks) if not tr: - tr = db.Track(path = path, folder = folder) + tr = db.Track(path = path, root_folder = folder, folder = self.__find_folder(path, folder)) self.__added_tracks += 1 else: tr = tr[0] @@ -85,6 +90,33 @@ class Scanner: return ar + def __find_folder(self, path, folder): + path = os.path.dirname(path) + fold = filter(lambda f: f.path == path, self.__folders) + if fold: + return fold[0] + + full_path = folder.path + path = path[len(folder.path) + 1:] + + for name in path.split(os.sep): + full_path = os.path.join(full_path, name) + fold = filter(lambda f: f.path == full_path, self.__folders) + if fold: + folder = fold[0] + else: + folder = db.Folder(root = False, name = name, path = full_path, parent = folder) + self.__folders.append(folder) + + return folder + + def __cleanup_folder(self, folder): + for f in folder.children: + self.__cleanup_folder(f) + if len(folder.children) == 0 and len(folder.tracks) == 0 and not folder.root: + folder.parent = None + self.__session.delete(folder) + def stats(self): return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks) diff --git a/web.py b/web.py index 755e695..1f4088b 100755 --- a/web.py +++ b/web.py @@ -23,7 +23,7 @@ def index(): flash('Not configured. Please create the first admin user') return redirect(url_for('add_user')) """ - return render_template('home.html', users = db.User.query.all(), folders = db.MusicFolder.query.all(), + return render_template('home.html', users = db.User.query.all(), folders = db.Folder.query.filter(db.Folder.root == True).all(), artists = db.Artist.query.order_by(db.Artist.name).all(), albums = db.Album.query.join(db.Album.artist).order_by(db.Artist.name, db.Album.name).all()) @@ -92,7 +92,7 @@ def add_folder(): if name in (None, ''): flash('The name is required.') error = True - elif db.MusicFolder.query.filter(db.MusicFolder.name == name).first(): + elif db.Folder.query.filter(db.Folder.name == name and db.Folder.root).first(): flash('There is already a folder with that name. Please pick another one.') error = True if path in (None, ''): @@ -103,14 +103,14 @@ def add_folder(): if not os.path.isdir(path): flash("The path '%s' doesn't exists or isn't a directory" % path) error = True - folder = db.MusicFolder.query.filter(db.MusicFolder.name == name).first() + folder = db.Folder.query.filter(db.Folder.path == path).first() if folder: - flash("This path is already registered with the name '%s'" % folder.name) + flash("This path is already registered") error = True if error: return render_template('addfolder.html') - folder = db.MusicFolder(name = name, path = path) + folder = db.Folder(root = True, name = name, path = path) db.session.add(folder) db.session.commit() flash("Folder '%s' created. You should now run a scan" % name) @@ -125,7 +125,7 @@ def del_folder(id): flash('Invalid folder id') return redirect(url_for('index')) - folder = db.MusicFolder.query.get(idid) + folder = db.Folder.query.get(idid) if folder is None: flash('No such folder') return redirect(url_for('index')) @@ -133,7 +133,7 @@ def del_folder(id): # delete associated tracks and prune empty albums/artists for artist in db.Artist.query.all(): for album in artist.albums[:]: - for track in filter(lambda t: t.folder.id == folder.id, album.tracks): + for track in filter(lambda t: t.root_folder.id == folder.id, album.tracks): album.tracks.remove(track) db.session.delete(track) if len(album.tracks) == 0: @@ -141,7 +141,13 @@ def del_folder(id): db.session.delete(album) if len(artist.albums) == 0: db.session.delete(artist) - db.session.delete(folder) + + def cleanup_folder(folder): + for f in folder.children: + cleanup_folder(f) + db.session.delete(folder) + + cleanup_folder(folder) db.session.commit() flash("Deleted folder '%s'" % folder.name) @@ -153,7 +159,7 @@ def del_folder(id): def scan_folder(id = None): s = Scanner(db.session) if id is None: - for folder in db.MusicFolder.query.all(): + for folder in db.Folder.query.filter(db.Folder.root == True).all(): s.scan(folder) s.prune(folder) else: @@ -163,8 +169,8 @@ def scan_folder(id = None): flash('Invalid folder id') return redirect(url_for('index')) - folder = db.MusicFolder.query.get(idid) - if folder is None: + folder = db.Folder.query.get(idid) + if folder is None or not folder.root: flash('No such folder') return redirect(url_for('index'))