|
|
|
@ -78,72 +78,49 @@ class PysonicFilesystemScanner(object):
|
|
|
|
|
|
|
|
|
|
def scan_dir(self, pid, root, path, dirs, files): |
|
|
|
|
""" |
|
|
|
|
Scan a single directory in the library. |
|
|
|
|
Scan a single directory in the library. Actually, this ignores all dirs that don't contain files. Dirs are |
|
|
|
|
interpreted as follows: |
|
|
|
|
- The library root is ignored |
|
|
|
|
- Empty dirs are ignored |
|
|
|
|
- Dirs containing files are assumed to be an album |
|
|
|
|
- Top level dirs in the library are assumed to be artists |
|
|
|
|
- Any dirs not following the above rules are transparently ignored |
|
|
|
|
- Files placed in an artist dir is an unhandled edge case TODO |
|
|
|
|
- Any files with an image extension in an album dir will be assumed to be the cover regardless of naming |
|
|
|
|
- TODO ignore dotfiles/dirs |
|
|
|
|
:param pid: parent id |
|
|
|
|
:param root: library root path |
|
|
|
|
:param path: scan location path, as a list of subdirs within the root |
|
|
|
|
:param dirs: dirs in the current path |
|
|
|
|
:param files: files in the current path |
|
|
|
|
""" |
|
|
|
|
# If there are no files then just bail |
|
|
|
|
if not files: |
|
|
|
|
# If this is the library root or an empty dir just bail |
|
|
|
|
if not path or not files: |
|
|
|
|
return |
|
|
|
|
# If it is the library root just bail |
|
|
|
|
if len(path) == 0: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# Guess an artist from the dir |
|
|
|
|
artist = path[0] |
|
|
|
|
|
|
|
|
|
# Guess an album from the dir, if possible |
|
|
|
|
album = None |
|
|
|
|
if len(path) > 1: |
|
|
|
|
album = path[-1] |
|
|
|
|
|
|
|
|
|
with closing(self.library.db.db.cursor()) as cursor: |
|
|
|
|
# Create artist entry |
|
|
|
|
artist_dirid = self.create_or_get_dbdir_tree(cursor, pid, [path[0]]) |
|
|
|
|
cursor.execute("SELECT * FROM artists WHERE dir = ?", (artist_dirid, )) |
|
|
|
|
row = cursor.fetchone() |
|
|
|
|
artist_id = None |
|
|
|
|
if row: |
|
|
|
|
artist_id = row['id'] |
|
|
|
|
else: |
|
|
|
|
cursor.execute("INSERT INTO artists (libraryid, dir, name) VALUES (?, ?, ?)", |
|
|
|
|
(pid, artist_dirid, artist)) |
|
|
|
|
artist_id = cursor.lastrowid |
|
|
|
|
artist_id, artist_dirid = self.create_or_get_artist(cursor, pid, path[0]) |
|
|
|
|
|
|
|
|
|
# Create album entry |
|
|
|
|
album_id = None |
|
|
|
|
album_dirid = self.create_or_get_dbdir_tree(cursor, pid, path) |
|
|
|
|
libpath = os.path.join(*path) |
|
|
|
|
album_dirid = None |
|
|
|
|
if album: |
|
|
|
|
cursor.execute("SELECT * FROM albums WHERE artistid = ? AND dir = ?", (artist_id, album_dirid, )) |
|
|
|
|
row = cursor.fetchone() |
|
|
|
|
if row: |
|
|
|
|
album_id = row['id'] |
|
|
|
|
else: |
|
|
|
|
cursor.execute("INSERT INTO albums (artistid, dir, name) VALUES (?, ?, ?)", |
|
|
|
|
(artist_id, album_dirid, path[-1])) |
|
|
|
|
album_id = cursor.lastrowid |
|
|
|
|
album_id, album_dirid = self.create_or_get_album(cursor, pid, path, artist_id) |
|
|
|
|
|
|
|
|
|
libpath = os.path.join(*path) |
|
|
|
|
|
|
|
|
|
new_files = False |
|
|
|
|
for file in files: |
|
|
|
|
if not any([file.endswith(".{}".format(i)) for i in MUSIC_EXTENSIONS]): |
|
|
|
|
for fname in files: |
|
|
|
|
if not any([fname.endswith(".{}".format(i)) for i in MUSIC_EXTENSIONS]): |
|
|
|
|
continue |
|
|
|
|
fpath = os.path.join(libpath, file) |
|
|
|
|
cursor.execute("SELECT id FROM songs WHERE file=?", (fpath, )) |
|
|
|
|
|
|
|
|
|
if not cursor.fetchall(): |
|
|
|
|
# We leave most fields blank now and return later |
|
|
|
|
cursor.execute("INSERT INTO songs (library, albumid, file, size, title) " |
|
|
|
|
"VALUES (?, ?, ?, ?, ?)", |
|
|
|
|
(pid, |
|
|
|
|
album_id, |
|
|
|
|
fpath, |
|
|
|
|
os.stat(os.path.join(root, fpath)).st_size, |
|
|
|
|
file, )) |
|
|
|
|
new_files = True |
|
|
|
|
new_files = self.add_music_if_new(cursor, pid, root, album_id, libpath, fname) or new_files |
|
|
|
|
|
|
|
|
|
# Create cover entry TODO we can probably skip this if there were no new audio files? |
|
|
|
|
if album_id: |
|
|
|
@ -161,6 +138,64 @@ class PysonicFilesystemScanner(object):
|
|
|
|
|
if new_files: # Commit after each dir IF audio files were found. no audio == dump the artist |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
def add_music_if_new(self, cursor, pid, root_dir, album_id, fdir, fname): |
|
|
|
|
fpath = os.path.join(fdir, fname) |
|
|
|
|
cursor.execute("SELECT id FROM songs WHERE file=?", (fpath, )) |
|
|
|
|
if not cursor.fetchall(): |
|
|
|
|
# We leave most fields blank now and return later |
|
|
|
|
# TODO probably not here but track file sizes and mark them for rescan on change |
|
|
|
|
cursor.execute("INSERT INTO songs (library, albumid, file, size, title) " |
|
|
|
|
"VALUES (?, ?, ?, ?, ?)", |
|
|
|
|
(pid, |
|
|
|
|
album_id, |
|
|
|
|
fpath, |
|
|
|
|
os.stat(os.path.join(root_dir, fpath)).st_size, |
|
|
|
|
fname, )) |
|
|
|
|
return True |
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
def create_or_get_artist(self, cursor, pid, dirname): |
|
|
|
|
""" |
|
|
|
|
Retrieve, creating if necessary, directory information about an artist. Return tuple contains the artist's ID |
|
|
|
|
and the dir id associated with the artist. |
|
|
|
|
:param cursor: sqlite cursor to use |
|
|
|
|
:param pid: root parent id we're working int |
|
|
|
|
:param dirname: name of the artist dir |
|
|
|
|
:return tuple: |
|
|
|
|
""" |
|
|
|
|
artist_dirid = self.create_or_get_dbdir_tree(cursor, pid, [dirname]) |
|
|
|
|
cursor.execute("SELECT * FROM artists WHERE dir = ?", (artist_dirid, )) |
|
|
|
|
row = cursor.fetchone() |
|
|
|
|
artist_id = None |
|
|
|
|
if row: |
|
|
|
|
artist_id = row['id'] |
|
|
|
|
else: |
|
|
|
|
cursor.execute("INSERT INTO artists (libraryid, dir, name) VALUES (?, ?, ?)", |
|
|
|
|
(pid, artist_dirid, dirname)) |
|
|
|
|
artist_id = cursor.lastrowid |
|
|
|
|
return artist_id, artist_dirid |
|
|
|
|
|
|
|
|
|
def create_or_get_album(self, cursor, pid, dirnames, artist_id): |
|
|
|
|
""" |
|
|
|
|
Retrieve, creating if necessary, directory information about an album. Return tuple contains the albums's ID |
|
|
|
|
and the dir id associated with the album. |
|
|
|
|
:param cursor: sqlite cursor to use |
|
|
|
|
:param pid: root parent id we're working int |
|
|
|
|
:param dirnames: list of directories from the root to the album dir |
|
|
|
|
:param artist_id: id of the artist the album belongs to |
|
|
|
|
:return tuple: |
|
|
|
|
""" |
|
|
|
|
album_dirid = self.create_or_get_dbdir_tree(cursor, pid, dirnames) |
|
|
|
|
cursor.execute("SELECT * FROM albums WHERE artistid = ? AND dir = ?", (artist_id, album_dirid, )) |
|
|
|
|
row = cursor.fetchone() |
|
|
|
|
if row: |
|
|
|
|
album_id = row['id'] |
|
|
|
|
else: |
|
|
|
|
cursor.execute("INSERT INTO albums (artistid, dir, name) VALUES (?, ?, ?)", |
|
|
|
|
(artist_id, album_dirid, dirnames[-1])) |
|
|
|
|
album_id = cursor.lastrowid |
|
|
|
|
return album_id, album_dirid |
|
|
|
|
|
|
|
|
|
def split_path(self, path): |
|
|
|
|
""" |
|
|
|
|
Given a path like /foo/bar, return ['foo', 'bar'] |
|
|
|
|