def fill_track_info(cls, filepath, file_contents): """Given the path and contents of a track, return a filled locker_pb2.Track. On problems, raise ValueError.""" track = locker_pb2.Track() track.client_id = cls.get_track_clientid(file_contents) extension = os.path.splitext(filepath)[1].upper() if extension: #Trim leading period if it exists (ie extension not empty). extension = extension[1:] if not hasattr(locker_pb2.Track, extension): raise ValueError("unsupported filetype") track.original_content_type = getattr(locker_pb2.Track, extension) track.estimated_size = os.path.getsize(filepath) track.last_modified_timestamp = int(os.path.getmtime(filepath)) #These are typically zeroed in my examples. track.play_count = 0 track.client_date_added = 0 track.recent_timestamp = 0 track.rating = locker_pb2.Track.NOT_RATED # star rating #Populate information about the encoding. audio = mutagen.File(filepath, easy=True) if audio is None: raise ValueError("could not open to read metadata") elif isinstance(audio, mutagen.asf.ASF): #WMA entries store more info than just the value. #Monkeypatch in a dict {key: value} to keep interface the same for all filetypes. asf_dict = { k: [ve.value for ve in v] for (k, v) in audio.tags.as_dict().items() } audio.tags = asf_dict track.duration_millis = int(audio.info.length * 1000) try: bitrate = int(audio.info.bitrate / 1000) except AttributeError: #mutagen doesn't provide bitrate for some lossless formats (eg FLAC), so # provide an estimation instead. This shouldn't matter too much; # the bitrate will always be > 320, which is the highest scan and match quality. bitrate = (track.estimated_size * 8) / track.duration_millis track.original_bit_rate = bitrate #Populate metadata. #Title is required. #If it's not in the metadata, the filename will be used. if "title" in audio: title = audio['title'][0] if isinstance(title, mutagen.asf.ASFUnicodeAttribute): title = title.value track.title = title else: #handle non-ascii path encodings. if not isinstance(filepath, unicode): #not sure if this is possible enc = utils.guess_str_encoding(filepath)[0] filepath = filepath.decode(enc) track.title = os.path.basename(filepath) if "date" in audio: try: track.year = int(audio["date"][0].split("-")[0]) except ValueError: #TODO log pass #Mass-populate the rest of the simple fields. #Merge shared and unshared fields into {mutagen: Track}. fields = dict({shared: shared for shared in cls.shared_fields}.items() + cls.field_map.items()) for mutagen_f, track_f in fields.items(): if mutagen_f in audio: setattr(track, track_f, audio[mutagen_f][0]) for mutagen_f, (track_f, track_total_f) in cls.count_fields.items(): if mutagen_f in audio: numstrs = audio[mutagen_f][0].split("/") setattr(track, track_f, int(numstrs[0])) if len(numstrs) == 2 and numstrs[1]: setattr(track, track_total_f, int(numstrs[1])) return track
def make_metadata_request(self, filenames): """Returns (Metadata protobuff, dictionary mapping ClientId to filename) for the given mp3s.""" filemap = {} #map clientid -> filename metadata = self.make_pb("metadata_request") for filename in filenames: if not filename.split(".")[-1].lower() == "mp3": LogController.get_logger("make_metadata_request").error( "cannot upload '%s' because it is not an mp3.", filename) continue track = metadata.tracks.add() #Eventually pull this to supported_filetypes audio = MP3(filename, ID3 = EasyID3) #The id is a 22 char hash of the file. It is found by: # stripping tags # getting an md5 sum # converting sum to base64 # removing trailing === #My implementation is _not_ the same hash the music manager will send; # they strip tags first. But files are differentiated across accounts, # so this shouldn't cause problems. #This will reupload files if their tags change. with open(filename, mode="rb") as f: file_contents = f.read() h = hashlib.md5(file_contents).digest() h = base64.encodestring(h)[:-3] id = h filemap[id] = filename track.id = id filesize = os.path.getsize(filename) track.fileSize = filesize track.bitrate = audio.info.bitrate / 1000 track.duration = int(audio.info.length * 1000) #GM requires at least a title. if "title" in audio: track.title = audio["title"][0] else: #attempt to handle unicode filenames. enc = utils.guess_str_encoding(filename)[0] track.title = filename.decode(enc).split(r'/')[-1] #TODO refactor if "album" in audio: track.album = audio["album"][0] if "artist" in audio: track.artist = audio["artist"][0] if "composer" in audio: track.composer = audio["composer"][0] #albumartist is 'performer' according to this guy: # https://github.com/plexinc-plugins/Scanners.bundle/commit/95cc0b9eeb7fa8fa77c36ffcf0ec51644a927700 if "performer" in audio: track.albumArtist = audio["performer"][0] if "genre" in audio: track.genre = audio["genre"][0] if "date" in audio: track.year = int(audio["date"][0].split("-")[0]) #this looks like an assumption if "bpm" in audio: track.beatsPerMinute = int(audio["bpm"][0]) #think these are assumptions: if "tracknumber" in audio: tracknumber = audio["tracknumber"][0].split("/") track.track = int(tracknumber[0]) if len(tracknumber) == 2 and tracknumber[1]: track.totalTracks = int(tracknumber[1]) if "discnumber" in audio: discnumber = audio["discnumber"][0].split("/") track.disc = int(discnumber[0]) if len(discnumber) == 2 and discnumber[1]: track.totalDiscs = int(discnumber[1]) return (metadata, filemap)
def fill_track_info(cls, filepath, file_contents): """Given the path and contents of a track, return a filled locker_pb2.Track. On problems, raise ValueError.""" track = locker_pb2.Track() track.client_id = cls.get_track_clientid(file_contents) extension = os.path.splitext(filepath)[1].upper() if extension: #Trim leading period if it exists (ie extension not empty). extension = extension[1:] if not hasattr(locker_pb2.Track, extension): raise ValueError("unsupported filetype") track.original_content_type = getattr(locker_pb2.Track, extension) track.estimated_size = os.path.getsize(filepath) track.last_modified_timestamp = int(os.path.getmtime(filepath)) #These are typically zeroed in my examples. track.play_count = 0 track.client_date_added = 0 track.recent_timestamp = 0 track.rating = locker_pb2.Track.NOT_RATED # star rating #Populate information about the encoding. audio = mutagen.File(filepath, easy=True) if audio is None: raise ValueError("could not open to read metadata") elif isinstance(audio, mutagen.asf.ASF): #WMA entries store more info than just the value. #Monkeypatch in a dict {key: value} to keep interface the same for all filetypes. asf_dict = {k: [ve.value for ve in v] for (k, v) in audio.tags.as_dict().items()} audio.tags = asf_dict track.duration_millis = int(audio.info.length * 1000) try: bitrate = int(audio.info.bitrate / 1000) except AttributeError: #mutagen doesn't provide bitrate for some lossless formats (eg FLAC), so # provide an estimation instead. This shouldn't matter too much; # the bitrate will always be > 320, which is the highest scan and match quality. bitrate = (track.estimated_size * 8) / track.duration_millis track.original_bit_rate = bitrate #Populate metadata. #Title is required. #If it's not in the metadata, the filename will be used. if "title" in audio: title = audio['title'][0] if isinstance(title, mutagen.asf.ASFUnicodeAttribute): title = title.value track.title = title else: #handle non-ascii path encodings. if not isinstance(filepath, unicode): #not sure if this is possible enc = utils.guess_str_encoding(filepath)[0] filepath = filepath.decode(enc) track.title = os.path.basename(filepath) if "date" in audio: try: track.year = int(audio["date"][0].split("-")[0]) except ValueError: #TODO log pass #Mass-populate the rest of the simple fields. #Merge shared and unshared fields into {mutagen: Track}. fields = dict( {shared: shared for shared in cls.shared_fields}.items() + cls.field_map.items() ) for mutagen_f, track_f in fields.items(): if mutagen_f in audio: setattr(track, track_f, audio[mutagen_f][0]) for mutagen_f, (track_f, track_total_f) in cls.count_fields.items(): if mutagen_f in audio: numstrs = audio[mutagen_f][0].split("/") setattr(track, track_f, int(numstrs[0])) if len(numstrs) == 2 and numstrs[1]: setattr(track, track_total_f, int(numstrs[1])) return track