class Song: """Represents a song in the library. """ ID3_COLUMNS = ("title", "artist", "album", "genre", "year") NON_ID3_COLUMNS = ("length", "date_modified") def __init__(self, file_path, title = None, artist = None, album = None, genre = None, year = None, override_id3 = True): """ Given an absolute file path, and data about the song a initialize a Song object. Parses ID3 tags for additional metadata if it exists. If the override_id3 is true, the given name and artist will override the name and artist contained in the ID3 tag. @param file_path: str @param title: str @param artist: str @param album: str @param genre: str @param year: int @param override_id3: bool """ self._file_path = file_path self._mp = None self._time = None # What time, in seconds, of the song playback to play at # Fill in column values, first by parsing ID3 tags and then manually self._columns = {} for tag_name, tag in zip(Song.ID3_COLUMNS, Song._get_ID3_tags(file_path)): self._columns[tag_name] = tag self._columns["length"] = int(MP3(file_path).info.length + 0.5) # Read length and round to nearest integer self._columns["date_modified"] = Song.get_date_modified(file_path) # If overriding, only do so for passed parameters if override_id3: self._columns["title"] = title if title is not None else self._columns["title"] self._columns["artist"] = artist if artist is not None else self._columns["artist"] self._columns["album"] = album if album is not None else self._columns["album"] self._columns["genre"] = genre if genre is not None else self._columns["genre"] self._columns["year"] = year if year is not None else self._columns["year"] def init(self): if self._mp is None: # Only initialize if not already initialized self._mp = MediaPlayer(self._file_path) def play(self, sleep_interval = 0.1): """ Plays this song. """ # Create the MediaPlayer on demand to save system resources (and prevent VLC from freaking out). if self._mp is None: raise SongException("Song not initialized") self._mp.play() if self._time is not None: self._mp.set_time(int(self._time * 1000)) # Seconds to milliseconds self._time = None # Sleep a bit to allow VLC to play the song, so self.playing() returns properly time.sleep(sleep_interval) def pause(self): """ Pauses this song, if it's playing. """ if self._mp is None: raise SongException("Song not initialized") self._mp.pause() def set_time(self, time): """ Sets the current play time of this song, so when the song is played (or if it's currently played) it will play from that time, or start playing from that time now if the song is currently playing. Given time should be in seconds. @param time: int or float """ if time < 0: raise SongException("Can't jump to negative timestamp") if time < self._columns["length"]: self._time = time if self.playing(): self.stop() self.init() self.play() def get_current_time(self): """ Returns the current play time, in seconds, of this song, if it's playing. @return float """ if self.playing(): return self._mp.get_time() / 1000 def set_volume(self, percentage): """ Sets the volume to the given percentage (between 0 and 100). @param percentage: int """ if self._mp is None: raise SongException("Song not initialized") elif percentage < 0 or percentage > 100: raise SongException("Percentage out of range") if self.playing(): self._mp.audio_set_volume(percentage) def get_volume(self): """ Returns the current volume of the song, if it's playing. @return int """ if self._mp is None: raise SongException("Song not initialized") if self.playing(): return self._mp.audio_get_volume() def mute(self): """ Mutes the song, if it's playing. """ if self._mp is None: raise SongException("Song not initialized") if self.playing(): self._mp.audio_set_mute(True) def unmute(self): """ Unmutes the song, if it's playing and is muted. """ if self._mp is None: raise SongException("Song not initialized") if self.playing(): self._mp.audio_set_mute(False) def playing(self): """ Returns if this song is playing or not (ie currently paused). @return: bool """ if self._mp is None: raise SongException("Song not initialized") return self._mp.is_playing() def reset(self): """ Resets the song to the beginning. """ if self._mp is None: raise SongException("Song not initialized") self._mp.stop() def stop(self): """ Terminates this song, freeing system resources and cleaning up. """ if self._mp is not None: self._mp.stop() self._mp = None def delete_from_disk(self): """ Deletes this song from the hard drive, returning if the deletion was successful. @return bool """ os.remove(self._file_path) def set_ID3_tag(tag, value): """ Sets this song's ID3 tag to the given value, returning if the set operation succeeded. @param tag: str @param value: str @return bool """ if tag not in ID3_COLUMNS: return False tags = EasyID3(self.file_path) tags[tag] = value tags.save() return True @staticmethod def _get_ID3_tags(file_path): """ Given a file path to an mp3 song, returns the ID3 tags for title, artist, album, genre, and year (in that order), or empty tuple if no tags are found. @param filename: str @return: tuple of ID3 tags """ ret = [None for _ in range(len(Song.ID3_COLUMNS))] try: tags = EasyID3(file_path) for tag in Song.ID3_COLUMNS: if tag in tags: ret.append(tags[tag][0]) else: ret.append(None) except ID3NoHeaderError: pass if not ret[Song.ID3_COLUMNS.index("title")]: ret[Song.ID3_COLUMNS.index("title")] = file_path.split("/")[-1][: -4] # Parse file name from absolute file path and delete file extension return tuple(ret) @staticmethod def get_date_modified(file_path): """ Gets the date modified of the file indicated by the given file path. @param file_path: str @return: datetime.datetime """ return datetime.fromtimestamp(os.path.getmtime(file_path)) @staticmethod def set_date_modified(file_path, date): """ Sets the "date modified" of a file. @param file_path: str @param date: datetime.datetime """ os.utime(file_path, (0, time.mktime(date.timetuple()))) def __str__(self): ret = self["title"] if self["artist"] is not None: ret += " - " + self["artist"] return ret def __eq__(self, other): if not isinstance(other, self.__class__): return False for col in self._columns: if col != "date_modified" and self[col] != other[col]: # Don't check if 'date modified' columns match return False return True # Getters, setters below def __getitem__(self, key): return self._columns[key] def __contains__(self, item): return item in self._columns