def test_subcommand_query_exclude(self): item = self.add_item_fixture(year=2016, day=13, month=3, comments=u'test comment') item.write() self.config['zero']['fields'] = ['comments'] self.config['zero']['update_database'] = False self.config['zero']['auto'] = False self.load_plugins('zero') self.run_command('zero', 'year: 0000') mf = MediaFile(syspath(item.path)) self.assertEqual(mf.year, 2016) self.assertEqual(mf.comments, u'test comment')
def test_write_date_components(self): mediafile = self._mediafile_fixture('full') mediafile.year = 2001 mediafile.month = 1 mediafile.day = 2 mediafile.original_year = 1999 mediafile.original_month = 12 mediafile.original_day = 30 mediafile.save() mediafile = MediaFile(mediafile.path) self.assertEqual(mediafile.year, 2001) self.assertEqual(mediafile.month, 1) self.assertEqual(mediafile.day, 2) self.assertEqual(mediafile.date, datetime.date(2001,1,2)) self.assertEqual(mediafile.original_year, 1999) self.assertEqual(mediafile.original_month, 12) self.assertEqual(mediafile.original_day, 30) self.assertEqual(mediafile.original_date, datetime.date(1999,12,30))
def test_no_patterns(self): self.config['zero']['fields'] = ['comments', 'month'] item = self.add_item_fixture( comments=u'test comment', title=u'Title', month=1, year=2000, ) item.write() self.load_plugins('zero') item.write() mf = MediaFile(syspath(item.path)) self.assertIsNone(mf.comments) self.assertIsNone(mf.month) self.assertEqual(mf.title, u'Title') self.assertEqual(mf.year, 2000)
def test_add_tiff_image(self): mediafile = self._mediafile_fixture('image') self.assertEqual(len(mediafile.images), 2) image = Image(data=self.tiff_data, desc=u'the composer', type=ImageType.composer) mediafile.images += [image] mediafile.save() mediafile = MediaFile(mediafile.path) self.assertEqual(len(mediafile.images), 3) # WMA does not preserve the order, so we have to work around this image = filter(lambda i: i.mime_type == 'image/tiff', mediafile.images)[0] self.assertExtendedImageAttributes(image, desc=u'the composer', type=ImageType.composer)
def album_imported(self, lib, album): self.write_album = True print_("Tagging Replay Gain: %s - %s" % (album.albumartist, album.album)) try: media_files = [MediaFile(item.path) for item in album.items()] media_files = [mf for mf in media_files if self.requires_gain(mf)] #calculate gain. Return value - track_data: array dictionary indexed by filename track_data, album_data = rgcalc.calculate( [mf.path for mf in media_files], True, self.ref_level) for mf in media_files: self.write_gain(mf, track_data, album_data) except (FileTypeError, UnreadableFileError, TypeError, ValueError), e: log.error("failed to calculate replaygain: %s ", e)
def test_collect_item_and_path(self): path = self.create_mediafile_fixture() mediafile = MediaFile(path) item, = self.add_item_fixtures() item.album = mediafile.album = 'AAA' item.tracktotal = mediafile.tracktotal = 5 item.title = 'TTT' mediafile.title = 'SSS' item.write() item.store() mediafile.save() out = self.run_with_output('--summarize', 'album:AAA', path) self.assertIn(u'album: AAA', out) self.assertIn(u'tracktotal: 5', out) self.assertIn(u'title: [various]', out) self.remove_mediafile_fixtures()
def _import_mtags(self, lib, opts, args): path, = args paths = [Path(path)] while paths: p = paths.pop(0) for child in p.iterdir(): if child.is_dir(): paths.append(child) continue loader = MTagLoader(child) al = [] for path, data in loader.items(): matching = lib.items(PathQuery('path', path)) if any(m.path == path.encode() for m in matching): continue print('add %r' % path) item = Item(path=path) mf = MediaFile(syspath(item.path)) for field in AUDIO_FIELDS: v = getattr(mf, field) item[field] = v values = {} for tag, converter in TAGS.items(): v = converter.get(data) if v is not None: values[tag] = v item[tag] = v for tag, converter in DEPENDENT_TAGS.items(): v = converter.get(data, values) if v is not None: item[tag] = v al.append(item) if al: #print(al) try: lib.add_album(al) except BaseException as e: import pdb pdb.post_mortem(e.__traceback__) return
def write(self, path=None, tags=None): """Write the item's metadata to a media file. All fields in `_media_fields` are written to disk according to the values on this object. `path` is the path of the mediafile to wirte the data to. It defaults to the item's path. `tags` is a dictionary of additional metadata the should be written to the file. Can raise either a `ReadError` or a `WriteError`. """ if path is None: path = self.path else: path = normpath(path) item_tags = dict(self) if tags is not None: item_tags.update(tags) plugins.send('write', item=self, path=path, tags=item_tags) try: mediafile = MediaFile(syspath(path), id3v23=beets.config['id3v23'].get(bool)) except (OSError, IOError, UnreadableFileError) as exc: raise ReadError(self.path, exc) mediafile.update(item_tags) try: mediafile.save() except (OSError, IOError, MutagenError) as exc: raise WriteError(self.path, exc) # The file has a new mtime. if path == self.path: self.mtime = self.current_mtime() plugins.send('after_write', item=self, path=path)
def test_empty_query_n_response_no_changes(self): item = self.add_item_fixture(year=2016, day=13, month=3, comments=u'test comment') item.write() item_id = item.id self.config['zero']['fields'] = ['comments'] self.config['zero']['update_database'] = True self.config['zero']['auto'] = False self.load_plugins('zero') with control_stdin('n'): self.run_command('zero') mf = MediaFile(syspath(item.path)) item = self.lib.get_item(item_id) self.assertEqual(item['year'], 2016) self.assertEqual(mf.year, 2016) self.assertEqual(mf.comments, u'test comment') self.assertEqual(item['comments'], u'test comment')
def create_importer(self, item_count=1, album_count=1): """Returns import session with fixtures. Copies the specified number of files to a subdirectory of ``self.temp_dir`` and creates a ``TestImportSession`` for this path. """ import_dir = os.path.join(self.temp_dir, 'import') if not os.path.isdir(import_dir): os.mkdir(import_dir) for i in range(album_count): album = u'album {0}'.format(i) album_dir = os.path.join(import_dir, album) os.mkdir(album_dir) for j in range(item_count): title = 'track {0}'.format(j) src = os.path.join(_common.RSRC, 'full.mp3') dest = os.path.join(album_dir, '{0}.mp3'.format(title)) shutil.copy(src, dest) mediafile = MediaFile(dest) mediafile.update({ 'artist': 'artist', 'albumartist': 'album artist', 'title': title, 'album': album, 'mb_albumid': None, 'mb_trackid': None, }) mediafile.save() config['import']['quiet'] = True config['import']['autotag'] = False config['import']['resume'] = False return TestImportSession(self.lib, logfile=None, query=None, paths=[import_dir])
def test_no_autotag_keeps_duplicate_album(self): config['import']['autotag'] = False item = self.lib.items().get() self.assertEqual(item.title, u't\xeftle 0') self.assertTrue(os.path.isfile(item.path)) # Imported item has the same artist and album as the one in the # library. import_file = os.path.join(self.importer.paths[0], 'album 0', 'track 0.mp3') import_file = MediaFile(import_file) import_file.artist = item['artist'] import_file.albumartist = item['artist'] import_file.album = item['album'] import_file.title = 'new title' self.importer.default_resolution = self.importer.Resolution.REMOVE self.importer.run() self.assertTrue(os.path.isfile(item.path)) self.assertEqual(len(self.lib.albums()), 2) self.assertEqual(len(self.lib.items()), 2)
def write(self): """Writes the item's metadata to the associated file. """ plugins.send('write', item=self) try: f = MediaFile(syspath(self.path)) except (OSError, IOError) as exc: raise util.FilesystemError(exc, 'read', (self.path, ), traceback.format_exc()) for key in ITEM_KEYS_WRITABLE: setattr(f, key, self[key]) try: f.save(id3v23=beets.config['id3v23'].get(bool)) except (OSError, IOError) as exc: raise util.FilesystemError(exc, 'write', (self.path, ), traceback.format_exc()) # The file has a new mtime. self.mtime = self.current_mtime()
def test_add_image_structure(self): mediafile = self._mediafile_fixture('image') self.assertEqual(len(mediafile.images), 2) image = Image(data=self.png_data, desc='the composer', type=Image.TYPES.composer) mediafile.images += [image] mediafile.save() mediafile = MediaFile(mediafile.path) self.assertEqual(len(mediafile.images), 3) # WMA does not preserve the order, so we have to work around this try: image = filter(lambda i: i.desc == 'the composer', mediafile.images)[0] except IndexError: image = None self.assertExtendedImageAttributes(image, desc='the composer', type=Image.TYPES.composer)
def write(self): """Write the item's metadata to the associated file. Can raise either a `ReadError` or a `WriteError`. """ try: f = MediaFile(syspath(self.path)) except (OSError, IOError) as exc: raise ReadError(self.path, exc) plugins.send('write', item=self) for key in ITEM_KEYS_WRITABLE: setattr(f, key, self[key]) try: f.save(id3v23=beets.config['id3v23'].get(bool)) except (OSError, IOError, MutagenError) as exc: raise WriteError(self.path, exc) # The file has a new mtime. self.mtime = self.current_mtime() plugins.send('after_write', item=self)
def _mediafile_fixture(self, name): name = name + '.' + self.extension src = os.path.join(_common.RSRC, name) target = os.path.join(self.temp_dir, name) shutil.copy(src, target) return MediaFile(target)
def file_metadata(path, release): # type: (str,sqlite3.Row)->Tuple[Mapping[str,str],bool] """ Prepare metadata dictionary for path substitution, based on file name, the tags stored within it and release info from the db. :param path: media file path :param release: database row with release info :return: pair (dict,boolean indicating if Vars.TITLE is taken from tags or file name). (None,None) if unable to parse the media file. """ try: f = MediaFile(path) except UnreadableFileError as ex: logger.info("MediaFile couldn't parse: %s (%s)", path.decode(headphones.SYS_ENCODING, 'replace'), str(ex)) return None, None res = MetadataDict() # add existing tags first, these will get overwritten by musicbrainz from db _media_file_to_dict(f, res) # raw database fields come next _row_to_dict(release, res) date, year = _date_year(release) if not f.disc: disc_number = '' else: disc_number = '%d' % f.disc if not f.track: track_number = '' else: track_number = '%02d' % f.track if not f.title: basename = os.path.basename( path.decode(headphones.SYS_ENCODING, 'replace')) title = os.path.splitext(basename)[0] from_metadata = False else: title = f.title from_metadata = True ext = os.path.splitext(path)[1] if release['ArtistName'] == "Various Artists" and f.artist: artist_name = f.artist else: artist_name = release['ArtistName'] if artist_name and artist_name.startswith('The '): sort_name = artist_name[4:] + ", The" else: sort_name = artist_name album_title = release['AlbumTitle'] override_values = { Vars.DISC: disc_number, Vars.TRACK: track_number, Vars.TITLE: title, Vars.ARTIST: artist_name, Vars.SORT_ARTIST: sort_name, Vars.ALBUM: album_title, Vars.YEAR: year, Vars.DATE: date, Vars.EXTENSION: ext, Vars.TITLE_LOWER: _lower(title), Vars.ARTIST_LOWER: _lower(artist_name), Vars.SORT_ARTIST_LOWER: _lower(sort_name), Vars.ALBUM_LOWER: _lower(album_title), } res.add_items(override_values.iteritems()) return res, from_metadata
def encode(albumPath): use_xld = headphones.CONFIG.ENCODER == 'xld' # Return if xld details not found if use_xld: (xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(headphones.CONFIG.XLDPROFILE) if not xldFormat: logger.error('Details for xld profile \'%s\' not found, files will not be re-encoded', xldProfile) return None else: xldProfile = None tempDirEncode = os.path.join(albumPath, "temp") musicFiles = [] musicFinalFiles = [] musicTempFiles = [] encoder = "" # Create temporary directory, but remove the old one first. try: if os.path.exists(tempDirEncode): shutil.rmtree(tempDirEncode) time.sleep(1) os.mkdir(tempDirEncode) except Exception as e: logger.exception("Unable to create temporary directory") return None for r, d, f in os.walk(albumPath): for music in f: if any(music.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): if not use_xld: encoderFormat = headphones.CONFIG.ENCODEROUTPUTFORMAT.encode(headphones.SYS_ENCODING) else: xldMusicFile = os.path.join(r, music) xldInfoMusic = MediaFile(xldMusicFile) encoderFormat = xldFormat if headphones.CONFIG.ENCODERLOSSLESS: ext = os.path.normpath(os.path.splitext(music)[1].lstrip(".")).lower() if not use_xld and ext == 'flac' or use_xld and (ext != xldFormat and (xldInfoMusic.bitrate / 1000 > 400)): musicFiles.append(os.path.join(r, music)) musicTemp = os.path.normpath(os.path.splitext(music)[0] + '.' + encoderFormat) musicTempFiles.append(os.path.join(tempDirEncode, musicTemp)) else: logger.debug('%s is already encoded', music) else: musicFiles.append(os.path.join(r, music)) musicTemp = os.path.normpath(os.path.splitext(music)[0] + '.' + encoderFormat) musicTempFiles.append(os.path.join(tempDirEncode, musicTemp)) if headphones.CONFIG.ENCODER_PATH: encoder = headphones.CONFIG.ENCODER_PATH.encode(headphones.SYS_ENCODING) else: if use_xld: encoder = os.path.join('/Applications', 'xld') elif headphones.CONFIG.ENCODER == 'lame': if headphones.SYS_PLATFORM == "win32": ## NEED THE DEFAULT LAME INSTALL ON WIN! encoder = "C:/Program Files/lame/lame.exe" else: encoder = "lame" elif headphones.CONFIG.ENCODER == 'ffmpeg': if headphones.SYS_PLATFORM == "win32": encoder = "C:/Program Files/ffmpeg/bin/ffmpeg.exe" else: encoder = "ffmpeg" elif headphones.CONFIG.ENCODER == 'libav': if headphones.SYS_PLATFORM == "win32": encoder = "C:/Program Files/libav/bin/avconv.exe" else: encoder = "avconv" i = 0 encoder_failed = False jobs = [] for music in musicFiles: infoMusic = MediaFile(music) encode = False if use_xld: if xldBitrate and (infoMusic.bitrate / 1000 <= xldBitrate): logger.info('%s has bitrate <= %skb, will not be re-encoded', music.decode(headphones.SYS_ENCODING, 'replace'), xldBitrate) else: encode = True elif headphones.CONFIG.ENCODER == 'lame': if not any(music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.' + x) for x in ["mp3", "wav"]): logger.warn('Lame cannot encode %s format for %s, use ffmpeg', os.path.splitext(music)[1], music) else: if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.mp3') and (int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE): logger.info('%s has bitrate <= %skb, will not be re-encoded', music, headphones.CONFIG.BITRATE) else: encode = True else: if headphones.CONFIG.ENCODEROUTPUTFORMAT == 'ogg': if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.ogg'): logger.warn('Cannot re-encode .ogg %s', music.decode(headphones.SYS_ENCODING, 'replace')) else: encode = True elif headphones.CONFIG.ENCODEROUTPUTFORMAT == 'mp3' or headphones.CONFIG.ENCODEROUTPUTFORMAT == 'm4a': if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.' + headphones.CONFIG.ENCODEROUTPUTFORMAT) and (int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE): logger.info('%s has bitrate <= %skb, will not be re-encoded', music, headphones.CONFIG.BITRATE) else: encode = True # encode if encode: job = (encoder, music, musicTempFiles[i], albumPath, xldProfile) jobs.append(job) else: musicFiles[i] = None musicTempFiles[i] = None i = i + 1 # Encode music files if len(jobs) > 0: processes = 1 # Use multicore if enabled if headphones.CONFIG.ENCODER_MULTICORE: if headphones.CONFIG.ENCODER_MULTICORE_COUNT == 0: processes = multiprocessing.cpu_count() else: processes = headphones.CONFIG.ENCODER_MULTICORE_COUNT logger.debug("Multi-core encoding enabled, spawning %d processes", processes) # Use multiprocessing only if it's worth the overhead. and if it is # enabled. If not, then use the old fashioned way. if processes > 1: with logger.listener(): pool = multiprocessing.Pool(processes=processes) results = pool.map_async(command_map, jobs) # No new processes will be created, so close it and wait for all # processes to finish pool.close() pool.join() # Retrieve the results results = results.get() else: results = map(command_map, jobs) # The results are either True or False, so determine if one is False encoder_failed = not all(results) musicFiles = filter(None, musicFiles) musicTempFiles = filter(None, musicTempFiles) # check all files to be encoded now exist in temp directory if not encoder_failed and musicTempFiles: for dest in musicTempFiles: if not os.path.exists(dest): encoder_failed = True logger.error("Encoded file '%s' does not exist in the destination temp directory", dest) # No errors, move from temp to parent if not encoder_failed and musicTempFiles: i = 0 for dest in musicTempFiles: if os.path.exists(dest): source = musicFiles[i] if headphones.CONFIG.DELETE_LOSSLESS_FILES: os.remove(source) check_dest = os.path.join(albumPath, os.path.split(dest)[1]) if os.path.exists(check_dest): os.remove(check_dest) try: shutil.move(dest, albumPath) except Exception as e: logger.error('Could not move %s to %s: %s', dest, albumPath, e) encoder_failed = True break i += 1 # remove temp directory shutil.rmtree(tempDirEncode) # Return with error if any encoding errors if encoder_failed: logger.error("One or more files failed to encode. Ensure you have the latest version of %s installed.", headphones.CONFIG.ENCODER) return None time.sleep(1) for r, d, f in os.walk(albumPath): for music in f: if any(music.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): musicFinalFiles.append(os.path.join(r, music)) if not musicTempFiles: logger.info('Encoding for folder \'%s\' is not required', albumPath) return musicFinalFiles
def extract_metadata(f): """ Scan all files in the given directory and decide on an artist, album and year based on the metadata. A decision is based on the number of different artists, albums and years found in the media files. """ from headphones import logger # Walk directory and scan all media files results = [] count = 0 for root, dirs, files in os.walk(f): for file in files: # Count the number of potential media files extension = os.path.splitext(file)[1].lower()[1:] if extension in headphones.MEDIA_FORMATS: count += 1 # Try to read the file info try: media_file = MediaFile(os.path.join(root, file)) except (FileTypeError, UnreadableFileError): # Probably not a media file continue # Append metadata to file artist = media_file.albumartist or media_file.artist album = media_file.album year = media_file.year if artist and album and year: results.append((artist.lower(), album.lower(), year)) # Verify results if len(results) == 0: logger.info("No metadata in media files found, ignoring.") return (None, None, None) # Require that some percentage of files have tags count_ratio = 0.75 if count < (count_ratio * len(results)): logger.info("Counted %d media files, but only %d have tags, ignoring.", count, len(results)) return (None, None, None) # Count distinct values artists = list(set([x[0] for x in results])) albums = list(set([x[1] for x in results])) years = list(set([x[2] for x in results])) # Remove things such as CD2 from album names if len(albums) > 1: new_albums = list(albums) # Replace occurences of e.g. CD1 for index, album in enumerate(new_albums): if RE_CD_ALBUM.search(album): old_album = new_albums[index] new_albums[index] = RE_CD_ALBUM.sub("", album).strip() logger.debug("Stripped albumd number identifier: %s -> %s", old_album, new_albums[index]) # Remove duplicates new_albums = list(set(new_albums)) # Safety check: if nothing has merged, then ignore the work. This can # happen if only one CD of a multi part CD is processed. if len(new_albums) < len(albums): albums = new_albums # All files have the same metadata, so it's trivial if len(artists) == 1 and len(albums) == 1: return (artists[0], albums[0], years[0]) # (Lots of) different artists. Could be a featuring album, so test for this. if len(artists) > 1 and len(albums) == 1: split_artists = [RE_FEATURING.split(x) for x in artists] featurings = [len(split_artist) - 1 for split_artist in split_artists] logger.info("Album seem to feature %d different artists", sum(featurings)) if sum(featurings) > 0: # Find the artist of which the least splits have been generated. # Ideally, this should be 0, which should be the album artist # itself. artist = split_artists[featurings.index(min(featurings))][0] # Done return (artist, albums[0], years[0]) # Not sure what to do here. logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists), len(albums), len(years)) logger.debug("Artists: %s, Albums: %s, Years: %s", artists, albums, years) return (None, None, None)
os.mkdir(tempDirEncode) except Exception, e: logger.exception("Unable to create temporary directory") return None for r, d, f in os.walk(albumPath): for music in f: if any(music.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): if not XLD: encoderFormat = headphones.ENCODEROUTPUTFORMAT.encode( headphones.SYS_ENCODING) else: xldMusicFile = os.path.join(r, music) xldInfoMusic = MediaFile(xldMusicFile) encoderFormat = xldFormat if (headphones.ENCODERLOSSLESS): ext = os.path.normpath( os.path.splitext(music)[1].lstrip(".")).lower() if not XLD and ext == 'flac' or XLD and ( ext != xldFormat and (xldInfoMusic.bitrate / 1000 > 400)): musicFiles.append(os.path.join(r, music)) musicTemp = os.path.normpath( os.path.splitext(music)[0] + '.' + encoderFormat) musicTempFiles.append( os.path.join(tempDirEncode, musicTemp)) else: logger.debug('%s is already encoded', music)
def test_write_initial_key_tag(self): self.modify(u"initial_key=C#m") item = self.lib.items().get() mediafile = MediaFile(item.path) self.assertEqual(mediafile.initial_key, u'C#m')
def test_embed_art_from_file(self): album = self.add_album_fixture() item = album.items()[0] self.run_command('embedart', '-f', self.artpath) mediafile = MediaFile(item.path) self.assertEqual(mediafile.images[0].data, self.image_data)
def test_properties_from_readable_fields(self): path = os.path.join(_common.RSRC, 'full.mp3') mediafile = MediaFile(path) for field in MediaFile.readable_fields(): self.assertTrue(hasattr(mediafile, field))
def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=False): if cron and not headphones.LIBRARYSCAN: return if not dir: if not headphones.MUSIC_DIR: return else: dir = headphones.MUSIC_DIR # If we're appending a dir, it's coming from the post processor which is # already bytestring if not append: dir = dir.encode(headphones.SYS_ENCODING) if not os.path.isdir(dir): logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, 'replace')) return myDB = db.DBConnection() new_artists = [] logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) if not append: # Clean up bad filepaths tracks = myDB.select( 'SELECT Location from alltracks WHERE Location IS NOT NULL UNION SELECT Location from tracks WHERE Location IS NOT NULL' ) for track in tracks: encoded_track_string = track['Location'].encode( headphones.SYS_ENCODING) if not os.path.isfile(encoded_track_string): myDB.action( 'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']]) myDB.action( 'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']]) del_have_tracks = myDB.select( 'SELECT Location, Matched, ArtistName from have') for track in del_have_tracks: encoded_track_string = track['Location'].encode( headphones.SYS_ENCODING, 'replace') if not os.path.isfile(encoded_track_string): if track['ArtistName']: #Make sure deleted files get accounted for when updating artist track counts new_artists.append(track['ArtistName']) myDB.action('DELETE FROM have WHERE Location=?', [track['Location']]) logger.info( 'File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(headphones.SYS_ENCODING, 'replace')) ###############myDB.action('DELETE from have') bitrates = [] song_list = [] new_song_count = 0 file_count = 0 latest_subdirectory = [] for r, d, f in os.walk(dir): #need to abuse slicing to get a copy of the list, doing it directly will skip the element after a deleted one #using a list comprehension will not work correctly for nested subdirectories (os.walk keeps its original list) for directory in d[:]: if directory.startswith("."): d.remove(directory) for files in f: # MEDIA_FORMATS = music file extensions, e.g. mp3, flac, etc if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): subdirectory = r.replace(dir, '') latest_subdirectory.append(subdirectory) if file_count == 0 and r.replace(dir, '') != '': logger.info( "[%s] Now scanning subdirectory %s" % (dir.decode(headphones.SYS_ENCODING, 'replace'), subdirectory.decode(headphones.SYS_ENCODING, 'replace'))) elif latest_subdirectory[file_count] != latest_subdirectory[ file_count - 1] and file_count != 0: logger.info( "[%s] Now scanning subdirectory %s" % (dir.decode(headphones.SYS_ENCODING, 'replace'), subdirectory.decode(headphones.SYS_ENCODING, 'replace'))) song = os.path.join(r, files) # We need the unicode path to use for logging, inserting into database unicode_song_path = song.decode(headphones.SYS_ENCODING, 'replace') # Try to read the metadata try: f = MediaFile(song) except (FileTypeError, UnreadableFileError): logger.error( "Cannot read file media file '%s'. It may be corrupted or not a media file.", unicode_song_path) continue # Grab the bitrates for the auto detect bit rate option if f.bitrate: bitrates.append(f.bitrate) # Use the album artist over the artist if available if f.albumartist: f_artist = f.albumartist elif f.artist: f_artist = f.artist else: f_artist = None # Add the song to our song list - # TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements) if f_artist and f.album and f.title: CleanName = helpers.cleanName(f_artist + ' ' + f.album + ' ' + f.title) else: CleanName = None controlValueDict = {'Location': unicode_song_path} newValueDict = { 'TrackID': f.mb_trackid, #'ReleaseID' : f.mb_albumid, 'ArtistName': f_artist, 'AlbumTitle': f.album, 'TrackNumber': f.track, 'TrackLength': f.length, 'Genre': f.genre, 'Date': f.date, 'TrackTitle': f.title, 'BitRate': f.bitrate, 'Format': f.format, 'CleanName': CleanName } #song_list.append(song_dict) check_exist_song = myDB.action( "SELECT * FROM have WHERE Location=?", [unicode_song_path]).fetchone() #Only attempt to match songs that are new, haven't yet been matched, or metadata has changed. if not check_exist_song: #This is a new track if f_artist: new_artists.append(f_artist) myDB.upsert("have", newValueDict, controlValueDict) new_song_count += 1 else: if check_exist_song[ 'ArtistName'] != f_artist or check_exist_song[ 'AlbumTitle'] != f.album or check_exist_song[ 'TrackTitle'] != f.title: #Important track metadata has been modified, need to run matcher again if f_artist and f_artist != check_exist_song[ 'ArtistName']: new_artists.append(f_artist) elif f_artist and f_artist == check_exist_song[ 'ArtistName'] and check_exist_song[ 'Matched'] != "Ignored": new_artists.append(f_artist) else: continue newValueDict['Matched'] = None myDB.upsert("have", newValueDict, controlValueDict) myDB.action( 'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, unicode_song_path]) myDB.action( 'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, unicode_song_path]) new_song_count += 1 else: #This track information hasn't changed if f_artist and check_exist_song[ 'Matched'] != "Ignored": new_artists.append(f_artist) file_count += 1 # Now we start track matching logger.info("%s new/modified songs found and added to the database" % new_song_count) song_list = myDB.action( "SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace') + "%"]) total_number_of_songs = myDB.action( "SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace') + "%"]).fetchone()[0] logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....") # Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid) # When we insert into the database, the tracks with the most specific information will overwrite the more general matches ##############song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID']) song_list = helpers.multikeysort(song_list, ['ArtistName', 'AlbumTitle']) # We'll use this to give a % completion, just because the track matching might take a while song_count = 0 latest_artist = [] for song in song_list: latest_artist.append(song['ArtistName']) if song_count == 0: logger.info("Now matching songs by %s" % song['ArtistName']) elif latest_artist[song_count] != latest_artist[song_count - 1] and song_count != 0: logger.info("Now matching songs by %s" % song['ArtistName']) #print song['ArtistName']+' - '+song['AlbumTitle']+' - '+song['TrackTitle'] song_count += 1 completion_percentage = float(song_count) / total_number_of_songs * 100 if completion_percentage % 10 == 0: logger.info("Track matching is " + str(completion_percentage) + "% complete") #THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT #EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION, #ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM #(IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT. if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']: track = myDB.action( 'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle'] ]).fetchone() have_updated = False if track: controlValueDict = { 'ArtistName': track['ArtistName'], 'AlbumTitle': track['AlbumTitle'], 'TrackTitle': track['TrackTitle'] } newValueDict = { 'Location': song['Location'], 'BitRate': song['BitRate'], 'Format': song['Format'] } myDB.upsert("tracks", newValueDict, controlValueDict) controlValueDict2 = {'Location': song['Location']} newValueDict2 = {'Matched': track['AlbumID']} myDB.upsert("have", newValueDict2, controlValueDict2) have_updated = True else: track = myDB.action( 'SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone() if track: controlValueDict = {'CleanName': track['CleanName']} newValueDict = { 'Location': song['Location'], 'BitRate': song['BitRate'], 'Format': song['Format'] } myDB.upsert("tracks", newValueDict, controlValueDict) controlValueDict2 = {'Location': song['Location']} newValueDict2 = {'Matched': track['AlbumID']} myDB.upsert("have", newValueDict2, controlValueDict2) have_updated = True else: controlValueDict2 = {'Location': song['Location']} newValueDict2 = {'Matched': "Failed"} myDB.upsert("have", newValueDict2, controlValueDict2) have_updated = True alltrack = myDB.action( 'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle'] ]).fetchone() if alltrack: controlValueDict = { 'ArtistName': alltrack['ArtistName'], 'AlbumTitle': alltrack['AlbumTitle'], 'TrackTitle': alltrack['TrackTitle'] } newValueDict = { 'Location': song['Location'], 'BitRate': song['BitRate'], 'Format': song['Format'] } myDB.upsert("alltracks", newValueDict, controlValueDict) controlValueDict2 = {'Location': song['Location']} newValueDict2 = {'Matched': alltrack['AlbumID']} myDB.upsert("have", newValueDict2, controlValueDict2) else: alltrack = myDB.action( 'SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone() if alltrack: controlValueDict = {'CleanName': alltrack['CleanName']} newValueDict = { 'Location': song['Location'], 'BitRate': song['BitRate'], 'Format': song['Format'] } myDB.upsert("alltracks", newValueDict, controlValueDict) controlValueDict2 = {'Location': song['Location']} newValueDict2 = {'Matched': alltrack['AlbumID']} myDB.upsert("have", newValueDict2, controlValueDict2) else: # alltracks may not exist if adding album manually, have should only be set to failed if not already updated in tracks if not have_updated: controlValueDict2 = {'Location': song['Location']} newValueDict2 = {'Matched': "Failed"} myDB.upsert("have", newValueDict2, controlValueDict2) else: controlValueDict2 = {'Location': song['Location']} newValueDict2 = {'Matched': "Failed"} myDB.upsert("have", newValueDict2, controlValueDict2) #######myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']]) logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) if not append: logger.info('Updating scanned artist track counts') # Clean up the new artist list unique_artists = {}.fromkeys(new_artists).keys() current_artists = myDB.select( 'SELECT ArtistName, ArtistID from artists') #There was a bug where artists with special characters (-,') would show up in new artists. artist_list = [ f for f in unique_artists if helpers.cleanName(f).lower() not in [helpers.cleanName(x[0]).lower() for x in current_artists] ] artists_checked = [ f for f in unique_artists if helpers.cleanName(f).lower() in [helpers.cleanName(x[0]).lower() for x in current_artists] ] # Update track counts for artist in artists_checked: # Have tracks are selected from tracks table and not all tracks because of duplicates # We update the track count upon an album switch to compliment this havetracks = len( myDB.select( 'SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', [artist]) ) + len( myDB.select( 'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist])) #Note, some people complain about having "artist have tracks" > # of tracks total in artist official releases # (can fix by getting rid of second len statement) myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist]) logger.info('Found %i new artists' % len(artist_list)) if len(artist_list): if headphones.ADD_ARTISTS: logger.info('Importing %i new artists' % len(artist_list)) importer.artistlist_to_mbids(artist_list) else: logger.info( 'To add these artists, go to Manage->Manage New Artists') #myDB.action('DELETE from newartists') for artist in artist_list: myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist]) if headphones.DETECT_BITRATE: headphones.PREFERRED_BITRATE = sum(bitrates) / len(bitrates) / 1000 else: # If we're appending a new album to the database, update the artists total track counts logger.info('Updating artist track counts') havetracks = len( myDB.select( 'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [ArtistID]) ) + len( myDB.select( 'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [ArtistName])) myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID]) if not append: update_album_status() lastfm.getSimilar() logger.info('Library scan complete')
for track in tracks: try: f = MediaFile(track['Location']) except Exception, e: logger.info("Exception from MediaFile for: " + track['Location'] + " : " + str(e)) continue controlValueDict = {"TrackID": track['TrackID']} newValueDict = {"Format": f.format} myDB.upsert("tracks", newValueDict, controlValueDict) logger.info('Finished finding media format for %s files' % len(tracks)) havetracks = myDB.select('SELECT * from have WHERE Location IS NOT NULL and Format IS NULL') if len(havetracks) > 0: logger.info('Finding media format for %s files' % len(havetracks)) for track in havetracks: try: f = MediaFile(track['Location']) except Exception, e: logger.info("Exception from MediaFile for: " + track['Location'] + " : " + str(e)) continue controlValueDict = {"TrackID": track['TrackID']} newValueDict = {"Format": f.format} myDB.upsert("have", newValueDict, controlValueDict) logger.info('Finished finding media format for %s files' % len(havetracks)) def getHybridRelease(fullreleaselist): """ Returns a dictionary of best group of tracks from the list of releases and earliest release date """ if len(fullreleaselist) == 0:
def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=False, artistScan=False): if cron and not headphones.CONFIG.LIBRARYSCAN: return if not dir: if not headphones.CONFIG.MUSIC_DIR: return else: dir = headphones.CONFIG.MUSIC_DIR # If we're appending a dir, it's coming from the post processor which is # already bytestring if not append or artistScan: dir = dir.encode(headphones.SYS_ENCODING) if not os.path.isdir(dir): logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, 'replace')) return myDB = db.DBConnection() new_artists = [] logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) if not append: # Clean up bad filepaths. Queries can take some time, ensure all results are loaded before processing if ArtistID: tracks = myDB.action( 'SELECT Location FROM alltracks WHERE ArtistID = ? AND Location IS NOT NULL UNION SELECT Location FROM tracks WHERE ArtistID = ? AND Location ' 'IS NOT NULL', [ArtistID, ArtistID]) else: tracks = myDB.action( 'SELECT Location FROM alltracks WHERE Location IS NOT NULL UNION SELECT Location FROM tracks WHERE Location IS NOT NULL' ) locations = [] for track in tracks: locations.append(track['Location']) for location in locations: encoded_track_string = location.encode(headphones.SYS_ENCODING, 'replace') if not os.path.isfile(encoded_track_string): myDB.action( 'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, location]) myDB.action( 'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, location]) if ArtistName: del_have_tracks = myDB.select( 'SELECT Location, Matched, ArtistName FROM have WHERE ArtistName = ? COLLATE NOCASE', [ArtistName]) else: del_have_tracks = myDB.select( 'SELECT Location, Matched, ArtistName FROM have') locations = [] for track in del_have_tracks: locations.append([track['Location'], track['ArtistName']]) for location in locations: encoded_track_string = location[0].encode(headphones.SYS_ENCODING, 'replace') if not os.path.isfile(encoded_track_string): if location[1]: # Make sure deleted files get accounted for when updating artist track counts new_artists.append(location[1]) myDB.action('DELETE FROM have WHERE Location=?', [location[0]]) logger.info( 'File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(headphones.SYS_ENCODING, 'replace')) bitrates = [] song_list = [] latest_subdirectory = [] new_song_count = 0 file_count = 0 for r, d, f in helpers.walk_directory(dir): # Filter paths based on config. Note that these methods work directly # on the inputs helpers.path_filter_patterns(d, headphones.CONFIG.IGNORED_FOLDERS, r) helpers.path_filter_patterns(f, headphones.CONFIG.IGNORED_FILES, r) for files in f: # MEDIA_FORMATS = music file extensions, e.g. mp3, flac, etc if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): subdirectory = r.replace(dir, '') latest_subdirectory.append(subdirectory) if file_count == 0 and r.replace(dir, '') != '': logger.info( "[%s] Now scanning subdirectory %s" % (dir.decode(headphones.SYS_ENCODING, 'replace'), subdirectory.decode(headphones.SYS_ENCODING, 'replace'))) elif latest_subdirectory[file_count] != latest_subdirectory[ file_count - 1] and file_count != 0: logger.info( "[%s] Now scanning subdirectory %s" % (dir.decode(headphones.SYS_ENCODING, 'replace'), subdirectory.decode(headphones.SYS_ENCODING, 'replace'))) song = os.path.join(r, files) # We need the unicode path to use for logging, inserting into database unicode_song_path = song.decode(headphones.SYS_ENCODING, 'replace') # Try to read the metadata try: f = MediaFile(song) except (FileTypeError, UnreadableFileError): logger.warning( "Cannot read media file '%s', skipping. It may be corrupted or not a media file.", unicode_song_path) continue except IOError: logger.warning( "Cannnot read media file '%s', skipping. Does the file exists?", unicode_song_path) continue # Grab the bitrates for the auto detect bit rate option if f.bitrate: bitrates.append(f.bitrate) # Use the album artist over the artist if available if f.albumartist: f_artist = f.albumartist elif f.artist: f_artist = f.artist else: f_artist = None # Add the song to our song list - # TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements) if f_artist and f.album and f.title: CleanName = helpers.clean_name(f_artist + ' ' + f.album + ' ' + f.title) else: CleanName = None controlValueDict = {'Location': unicode_song_path} newValueDict = { 'TrackID': f.mb_trackid, # 'ReleaseID' : f.mb_albumid, 'ArtistName': f_artist, 'AlbumTitle': f.album, 'TrackNumber': f.track, 'TrackLength': f.length, 'Genre': f.genre, 'Date': f.date, 'TrackTitle': f.title, 'BitRate': f.bitrate, 'Format': f.format, 'CleanName': CleanName } # song_list.append(song_dict) check_exist_song = myDB.action( "SELECT * FROM have WHERE Location=?", [unicode_song_path]).fetchone() # Only attempt to match songs that are new, haven't yet been matched, or metadata has changed. if not check_exist_song: # This is a new track if f_artist: new_artists.append(f_artist) myDB.upsert("have", newValueDict, controlValueDict) new_song_count += 1 else: if check_exist_song[ 'ArtistName'] != f_artist or check_exist_song[ 'AlbumTitle'] != f.album or check_exist_song[ 'TrackTitle'] != f.title: # Important track metadata has been modified, need to run matcher again if f_artist and f_artist != check_exist_song[ 'ArtistName']: new_artists.append(f_artist) elif f_artist and f_artist == check_exist_song['ArtistName'] and \ check_exist_song['Matched'] != "Ignored": new_artists.append(f_artist) else: continue newValueDict['Matched'] = None myDB.upsert("have", newValueDict, controlValueDict) myDB.action( 'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, unicode_song_path]) myDB.action( 'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, unicode_song_path]) new_song_count += 1 else: # This track information hasn't changed if f_artist and check_exist_song[ 'Matched'] != "Ignored": new_artists.append(f_artist) file_count += 1 # Now we start track matching logger.info("%s new/modified songs found and added to the database" % new_song_count) song_list = myDB.action( "SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace') + "%"]) total_number_of_songs = \ myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace') + "%"]).fetchone()[0] logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....") # Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid) # When we insert into the database, the tracks with the most specific information will overwrite the more general matches # song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID']) song_list = helpers.multikeysort(song_list, ['ArtistName', 'AlbumTitle']) # We'll use this to give a % completion, just because the track matching might take a while song_count = 0 latest_artist = [] last_completion_percentage = 0 prev_artist_name = None artistid = None for song in song_list: latest_artist.append(song['ArtistName']) if song_count == 0: logger.info("Now matching songs by %s" % song['ArtistName']) elif latest_artist[song_count] != latest_artist[song_count - 1] and song_count != 0: logger.info("Now matching songs by %s" % song['ArtistName']) song_count += 1 completion_percentage = math.floor( float(song_count) / total_number_of_songs * 1000) / 10 if completion_percentage >= (last_completion_percentage + 10): logger.info("Track matching is " + str(completion_percentage) + "% complete") last_completion_percentage = completion_percentage # THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT # EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION, # ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM # (IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT. albumid = None if song['ArtistName'] and song['CleanName']: artist_name = song['ArtistName'] clean_name = song['CleanName'] # Only update if artist is in the db if artist_name != prev_artist_name: prev_artist_name = artist_name artistid = None artist_lookup = "\"" + artist_name.replace("\"", "\"\"") + "\"" try: dbartist = myDB.select( 'SELECT DISTINCT ArtistID, ArtistName FROM artists WHERE ArtistName LIKE ' + artist_lookup + '') except: dbartist = None if not dbartist: dbartist = myDB.select( 'SELECT DISTINCT ArtistID, ArtistName FROM tracks WHERE CleanName = ?', [clean_name]) if not dbartist: dbartist = myDB.select( 'SELECT DISTINCT ArtistID, ArtistName FROM alltracks WHERE CleanName = ?', [clean_name]) if not dbartist: clean_artist = helpers.clean_name(artist_name) if clean_artist: dbartist = myDB.select( 'SELECT DISTINCT ArtistID, ArtistName FROM tracks WHERE CleanName >= ? and CleanName < ?', [clean_artist, clean_artist + '{']) if not dbartist: dbartist = myDB.select( 'SELECT DISTINCT ArtistID, ArtistName FROM alltracks WHERE CleanName >= ? and CleanName < ?', [clean_artist, clean_artist + '{']) if dbartist: artistid = dbartist[0][0] if artistid: # This was previously using Artist, Album, Title with a SELECT LIKE ? and was not using an index # (Possible issue: https://stackoverflow.com/questions/37845854/python-sqlite3-not-using-index-with-like) # Now selects/updates using CleanName index (may have to revert if not working) # matching on CleanName should be enough, ensure it's the same artist just in case # Update tracks track = myDB.action( 'SELECT AlbumID, ArtistName FROM tracks WHERE CleanName = ? AND ArtistID = ?', [clean_name, artistid]).fetchone() if track: albumid = track['AlbumID'] myDB.action( 'UPDATE tracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ? AND ArtistID = ?', [ song['Location'], song['BitRate'], song['Format'], clean_name, artistid ]) # Update alltracks alltrack = myDB.action( 'SELECT AlbumID, ArtistName FROM alltracks WHERE CleanName = ? AND ArtistID = ?', [clean_name, artistid]).fetchone() if alltrack: albumid = alltrack['AlbumID'] myDB.action( 'UPDATE alltracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ? AND ArtistID = ?', [ song['Location'], song['BitRate'], song['Format'], clean_name, artistid ]) # Update have controlValueDict2 = {'Location': song['Location']} if albumid: newValueDict2 = {'Matched': albumid} else: newValueDict2 = {'Matched': "Failed"} myDB.upsert("have", newValueDict2, controlValueDict2) # myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']]) logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) if not append or artistScan: logger.info('Updating scanned artist track counts') # Clean up the new artist list unique_artists = {}.fromkeys(new_artists).keys() # # Don't think we need to do this, check the db instead below # # # artist scan # if ArtistName: # current_artists = [[ArtistName]] # # directory scan # else: # current_artists = myDB.select('SELECT ArtistName, ArtistID FROM artists WHERE ArtistName IS NOT NULL') # # # There was a bug where artists with special characters (-,') would show up in new artists. # # # artist_list = scanned artists not in the db # artist_list = [ # x for x in unique_artists # if helpers.clean_name(x).lower() not in [ # helpers.clean_name(y[0]).lower() # for y in current_artists # ] # ] # # # artists_checked = scanned artists that exist in the db # artists_checked = [ # x for x in unique_artists # if helpers.clean_name(x).lower() in [ # helpers.clean_name(y[0]).lower() # for y in current_artists # ] # ] new_artist_list = [] for artist in unique_artists: if not artist: continue logger.info('Processing artist: %s' % artist) # check if artist is already in the db artist_lookup = "\"" + artist.replace("\"", "\"\"") + "\"" try: dbartist = myDB.select( 'SELECT DISTINCT ArtistID, ArtistName FROM artists WHERE ArtistName LIKE ' + artist_lookup + '') except: dbartist = None if not dbartist: clean_artist = helpers.clean_name(artist) if clean_artist: dbartist = myDB.select( 'SELECT DISTINCT ArtistID, ArtistName FROM tracks WHERE CleanName >= ? and CleanName < ?', [clean_artist, clean_artist + '{']) if not dbartist: dbartist = myDB.select( 'SELECT DISTINCT ArtistID, ArtistName FROM alltracks WHERE CleanName >= ? and CleanName < ?', [clean_artist, clean_artist + '{']) # new artist not in db, add to list if not dbartist: new_artist_list.append(artist) else: # artist in db, update have track counts artistid = dbartist[0][0] # Have tracks are selected from tracks table and not all tracks because of duplicates # We update the track count upon an album switch to compliment this # havetracks = ( # len(myDB.select( # 'SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', # [artist])) + len(myDB.select( # 'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', # [artist])) # ) try: havetracks = (len( myDB.select( 'SELECT ArtistID From tracks WHERE ArtistID = ? AND Location IS NOT NULL', [artistid]) ) + len( myDB.select( 'SELECT ArtistName FROM have WHERE ArtistName LIKE ' + artist_lookup + ' AND Matched = "Failed"'))) except Exception as e: logger.warn('Error updating counts for artist: %s: %s' % (artist, e)) # Note: some people complain about having "artist have tracks" > # of tracks total in artist official releases # (can fix by getting rid of second len statement) if havetracks: myDB.action( 'UPDATE artists SET HaveTracks = ? WHERE ArtistID = ?', [havetracks, artistid]) # Update albums to downloaded update_album_status(ArtistID=artistid) logger.info('Found %i new artists' % len(new_artist_list)) # Add scanned artists not in the db if new_artist_list: if headphones.CONFIG.AUTO_ADD_ARTISTS: logger.info('Importing %i new artists' % len(new_artist_list)) importer.artistlist_to_mbids(new_artist_list) else: logger.info( 'To add these artists, go to Manage->Manage New Artists') # myDB.action('DELETE from newartists') for artist in new_artist_list: myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist]) if headphones.CONFIG.DETECT_BITRATE and bitrates: headphones.CONFIG.PREFERRED_BITRATE = sum(bitrates) / len( bitrates) / 1000 else: # If we're appending a new album to the database, update the artists total track counts logger.info('Updating artist track counts') artist_lookup = "\"" + ArtistName.replace("\"", "\"\"") + "\"" try: havetracks = len( myDB.select( 'SELECT ArtistID FROM tracks WHERE ArtistID = ? AND Location IS NOT NULL', [ArtistID]) ) + len( myDB.select( 'SELECT ArtistName FROM have WHERE ArtistName LIKE ' + artist_lookup + ' AND Matched = "Failed"')) except Exception as e: logger.warn('Error updating counts for artist: %s: %s' % (ArtistName, e)) if havetracks: myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID]) # Moved above to call for each artist # if not append: # update_album_status() if not append and not artistScan: lastfm.getSimilar() if ArtistName: logger.info('Scanning complete for artist: %s', ArtistName) else: logger.info('Library scan complete')
def test_external(self): external_dir = os.path.join(self.mkdtemp(), 'myplayer') self.config['convert']['formats'] = { 'aac': { 'command': 'bash -c "cp \'$source\' \'$dest\';' + 'printf ISAAC >> \'$dest\'"', 'extension': 'm4a' }, } self.config['alternatives'] = { 'myplayer': { 'directory': external_dir, 'paths': {'default': u'$artist/$title'}, 'formats': u'aac mp3', 'query': u'onplayer:true', 'removable': True, } } self.add_album(artist='Bach', title='was mp3', format='mp3') self.add_album(artist='Bach', title='was m4a', format='m4a') self.add_album(artist='Bach', title='was ogg', format='ogg') self.add_album(artist='Beethoven', title='was ogg', format='ogg') external_from_mp3 = bytestring_path( os.path.join(external_dir, 'Bach', 'was mp3.mp3')) external_from_m4a = bytestring_path( os.path.join(external_dir, 'Bach', 'was m4a.m4a')) external_from_ogg = bytestring_path( os.path.join(external_dir, 'Bach', 'was ogg.m4a')) external_beet = bytestring_path( os.path.join(external_dir, 'Beethoven', 'was ogg.m4a')) self.runcli('modify', '--yes', 'onplayer=true', 'artist:Bach') with control_stdin('y'): out = self.runcli('alt', 'update', 'myplayer') self.assertIn('Do you want to create the collection?', out) self.assertNotFileTag(external_from_mp3, b'ISAAC') self.assertNotFileTag(external_from_m4a, b'ISAAC') self.assertFileTag(external_from_ogg, b'ISAAC') self.assertFalse(os.path.isfile(external_beet)) self.runcli('modify', '--yes', 'composer=JSB', 'artist:Bach') list_output = self.runcli( 'alt', 'list-tracks', 'myplayer', '--format', '$artist $title') self.assertEqual(list_output, '\n'.join( ['Bach was mp3', 'Bach was m4a', 'Bach was ogg', ''])) self.runcli('alt', 'update', 'myplayer') mediafile = MediaFile(syspath(external_from_ogg)) self.assertEqual(mediafile.composer, 'JSB') self.runcli('modify', '--yes', 'onplayer!', 'artist:Bach') self.runcli('modify', '--album', '--yes', 'onplayer=true', 'albumartist:Beethoven') self.runcli('alt', 'update', 'myplayer') list_output = self.runcli( 'alt', 'list-tracks', 'myplayer', '--format', '$artist') self.assertEqual(list_output, 'Beethoven\n') self.assertFalse(os.path.isfile(external_from_mp3)) self.assertFalse(os.path.isfile(external_from_m4a)) self.assertFalse(os.path.isfile(external_from_ogg)) self.assertFileTag(external_beet, b'ISAAC')
def test_write_custom_tags(self): item = self.add_item_fixture(artist='old artist') item.write(tags={'artist': 'new artist'}) self.assertNotEqual(item.artist, 'new artist') self.assertEqual(MediaFile(item.path).artist, 'new artist')
def modifyFile(self, path, title='a different title'): mediafile = MediaFile(path) mediafile.title = title mediafile.save()