# ======================================================================= # GTUNES.PY (J. GARVIN) # Functions for the GarvinTunes Music Management System. # Must be imported into your main program to use these functions. # All tracks must be in GAR file format (see separate description). # Update 2023-01-17: fixed issue with is_valid_data off-shift in indices # due to missing file name in metadata (via Mitchell) # Update 2023-01-22: save_metadata was writing file name to file, # removed that issue (also via Mitchell) # ----------------------------------------------------------------------- # # ⚠️ ⚠️⚠️ WARNING! ⚠️⚠️⚠️ # # MODIFYING ANYTHING IN THIS FILE MAY BREAK YOUR CODE! CONTACT ME IF # YOU DISCOVER A PROBLEM RELATED TO THE CODE IN THIS FILE! # ======================================================================= import glob # global file-handling library # ======================================================================= # LIBRARY- AND TRACK-SPECIFIC FUNCTIONS # Call these functions from your main program to load and play music # files as necessary. # ======================================================================= # ----------------------------------------------------------------------- # LOAD_LIBRARY # Populate the music library with metadata from all GAR files in the # current directory. The library itself is a list. Each entry in the # library is a list containing metadata from each track (but does not) # contain the song data itself). Thus, the library is a list of lists. # Each track entry in the library has the following format: # FILENAME, ARTIST, ALBUM, YEAR, GENRE, TRACKNAME, TRACKNUM # e.g. ['abc.gar','Alphateers','Alphabet Soup','2014','Pop','ABC','1'] # To save space, the library does not contain any actual song data. # If you wish to obtain song data, you must call load_track manually. # Returns the library as a (possibly empty) list. # ----------------------------------------------------------------------- def load_library(): library = [] print("Loading music library...") for file_name in glob.glob('*.gar'): print(f"Found {file_name.upper()}... ", end="") track_data = load_track(file_name) if track_data: library.append(track_data[0]) print("loaded successfully.") if len(library) == 0: print("NO TRACKS FOUND IN FOLDER.") else: print("Library successfully loaded!") return library # ----------------------------------------------------------------------- # IS_VALID_DATA # Checks whether the metadata and song data for a given track are in the # proper format. Metadata must conform to the following specifications: # File name: no restrictions. # Artist name: no restrictions. # Album name: no restrictions. # Release year: four-digit positive integer. # Genre: no restrictions. # Track name: no restrictions. # Track number: positive integer. # Song data must consist of a number of characters equal to some multiple # of 10. Returns True if valid, False if not. # ----------------------------------------------------------------------- def is_valid_data(data): data = [str(entry) for entry in data] if (len(data) == 8 and data[3].isdigit() and len(data[3]) == 4 and data[6].isdigit() and len(data[7]) % 10 == 0): return True return False # ----------------------------------------------------------------------- # LOAD_TRACK # Load a single track, given its filename/path. e.g. the command # load_track('abc.gar') will obtain the metadata (artist, title, etc.) # and song data for that file. Returns both the metadata (list) and the # song data (string) if the file is valid, otherwise returns None. # ----------------------------------------------------------------------- def load_track(file_name): try: with open(file_name, 'r') as file: data = file.read().strip().split('\n') if is_valid_data([file_name] + data): # Note: the following line causes a type warning in mypy # as it assumes that lists are homogeneous. metadata = [file_name] + data[:2] + [int(data[2])] + data[3:5] + [int(data[5])] song_data = data[6] return metadata, song_data else: print(f"FILE {file_name.upper()} IS NOT A VALID GAR FILE.") return None except FileNotFoundError: print(f"CANNOT FIND FILE {file_name.upper()}.") return None except OSError: print(f"CANNOT READ FILE {file_name.upper()}.") return None # ----------------------------------------------------------------------- # PLAY_TRACK # "Plays" a track, given its filename/path (stored in library). # e.g. the command play_track('abc.gar') will "play" that file. # Does not return anything. # ----------------------------------------------------------------------- def play_track(file_name): track_data = load_track(file_name) if track_data: metadata, song_data = track_data print(f"Now playing {metadata[4]} by {metadata[0]} ...") for char in song_data: print(char, end="") print(f"\nDone playing {file_name.upper()}.") # ======================================================================= # EXTENSION FUNCTIONS # Don't worry about these until your program handles basic tasks like # loading and playing songs. # ======================================================================= # ----------------------------------------------------------------------- # SAVE_METADATA # Writes new metadata (artist, album, etc.) to a file, given its # filename/path (stored in library). Does not update any song data. # e.g. the command save_metadata('abc.gar', METADATA) will write new # values for that file, where METADATA is a list with the new info. # Does NOT update any information in the library itself. You will need # to update the information manually. Returns nothing. # ----------------------------------------------------------------------- def save_metadata(file_name, metadata): _, song_data = load_track(file_name) if song_data: data = metadata + [song_data] if is_valid_data(data): try: with open(file_name, 'w') as file: print(f"Writing metadata for {file_name.upper()}... ", end="") for entry in data[1:]: file.write(f"{str(entry)}\n") print("success!") except OSError: print(f"COULD NOT WRITE METADATA TO {file_name.upper()}.") else: print(f"INVALID METADATA, DID NOT UPDATE {file_name.upper()}.") # ----------------------------------------------------------------------- # IS_VALID_PLAYLIST # Ensures that each line of a playlist contains a file name that # references a GAR file. No checks are done to ensure that the file # exists. Returns True if valid, False if not. # ----------------------------------------------------------------------- def is_valid_playlist(playlist): for track in playlist: if not track.lower().endswith(".gar"): return False else: return True # ----------------------------------------------------------------------- # Load a playlist, given a valid filename. A playlist consists of # one or more filenames, which can be loaded one after another. # e.g. the command load_playlist(mylist.pl) loads that playlist. # Returns the playlist (list) if the file was found, None if not. # ----------------------------------------------------------------------- def load_playlist(file_name): try: with open(file_name, 'r') as file: playlist = file.read().strip().split('\n') if len(playlist) == 0: print(f"PLAYLIST {file_name.upper()} IS EMPTY.") return None elif is_valid_playlist(playlist): return playlist else: print(f"PLAYLIST {file_name.upper()} CONTAINS INVALID DATA.") return None except FileNotFoundError: print(f"CANNOT FIND PLAYLIST {file_name.upper()}.") return None except OSError: print(f"CANNOT READ PLAYLIST {file_name.upper()}.") return None # ----------------------------------------------------------------------- # Save a playlist, given a valid path/filename. # e.g. the command save_playlist(mylist.pl, playlist) saves the contents # of the playlist to a file called mylist.pl. Returns nothing. # ----------------------------------------------------------------------- def save_playlist(file_name, playlist): try: with open(file_name, 'w') as file: print(f"Saving playlist {file_name.upper()}... ", end="") for entry in playlist: file.write(f"{str(entry)}\n") print("success!") except OSError: print(f"COULD NOT WRITE TO PLAYLIST {file_name.upper()}.")