def get_all_projects(): Log("Gathering local projects..") local = Index.get_local_projects() Log("Gathering remote projects..") remote = Index.get_remote_projects() projects = list(remote) projects.extend(x for x in local if not x.is_remote()) return projects
def download_project(self): Log("Downloading project..") # Download new cache if not Drive.download(self.entry.data["id"], self.get_cache_file()): Log("An error occurred when trying to download the project..") Log.press_enter() # something went wrong while downloading return False # Update local cache 'db.json' with new hash Hash.set_project_hash(self, self.entry.data["hash"]) return True
def create(folderpath, skip_if_exists=True): folderpath = Path(folderpath) if folderpath.is_file(): Log(f'Can not create folder "{folderpath}".. This is already a file!') elif folderpath.is_dir(): if skip_if_exists: return True else: Log(f'Can not create folder "{folderpath}".. This folder already exists!') else: os.makedirs(folderpath.absolute()) return True return False
def is_dirty(self): # If there is locally saved changes that have yet to be uploaded to the cloud if Dev.get("ALL_IS_DIRTY"): return True if not self.get_song_file().exists(): Log("Studio One project file doesn't exist!", "warning") return False if not self.get_song_file(version="original").exists(): Log("Studio One 'original' project file doesn't exist!", "warning") return False return not filecmp.cmp(self.get_song_file(), self.get_song_file(version="original"), shallow=False)
def create_required_folders(self, temp=False, clean=True): # 'temp=True' will focus on the temp/<song> directory # 'temp=False' will focus on the extracted_songs/<song> directory # 'clean=True' will delete the folders first, then recreate them # Need to make sure these folders exist Log("Creating necessary folders") if temp: # Clean out temp dir if clean: Folder.delete(self.get_temp_dir()) Folder.create(self.get_temp_dir()) else: # Create new extracted dir, remove if exists if clean: Folder.delete(self.get_root_dir()) Folder.create(self.get_root_dir()) for location in ["Media", "Bounces", "Mixdown"]: if temp: Folder.create(self.get_temp_dir() / location) Folder.create(self.get_root_dir() / location) return True
def start_main_program(): main = Path(".main.py") if main.exists(): ans = Run.prg("python", f'"{main.absolute()}"', useSubprocess=True) if ans == 0: # There was an error Slack.upload_log() raise Exception( "\n\nThere was an error.. Contact your administrator.\n\n") else: Log(".main.py Does not exist!!", "warning") Dialog(title="Fatal Error!", body=[ f'"{main.absolute()}" doesnt exist!', f'\n', f'\n', f'Something went terribly wrong.. Contact your', f'administrator.', f'\n', f'\n', ], clear=False).press_enter() Slack.upload_log()
def get(key): data = Dev._get_data() if key in data: if "DEVELOPMENT" in data and not data["DEVELOPMENT"]: return False if data[key] and key != "DEVELOPMENT": Log(f'Development Mode: {key}', "warning") Log.press_enter() return data[key] else: data[key] = False File.set_json(FILEPATH, data) return False
def dialog_copy_new_name(self): dialog = Dialog( title=f'Make Duplicate of "{self.entry.name}"', body="Please enter a new name for your duplicate project.") new_name = dialog.get_result("New Name").lower() project_names = [ x.name.lower() for x in Folder.ls_folders(self.get_root_dir().parent) ] if not new_name in project_names: return new_name Log("That project name already exists.. Please try again!") Log.press_enter() return self.dialog_copy_new_name()
def __init__(self, title, body, clear=True): self.stack = [ f'', f'', self.create_title(title), f'', self.create_body(body), ] Log(f'Dialog: "{title}"', 'notice', quiet=True) self.show(self.stack, clear)
def get_result(self): ans = input(f' : ').lower() if ans == "b" and self.back: Log(f'Menu Answer: "back"', 'sub', quiet=True) print(f'') return "back" elif ans.isnumeric() and int(ans) <= len(self.options): result = (int(ans) -1) Log(f'Menu Answer: "{self.options[result]}"', 'sub', quiet=True) print(f'') return result print(f'') print(f'"{ans}" is not a valid option!') print(f'') Log(f'Menu Answer Not Valid!: "{ans}"', 'sub', quiet=True) Log.press_enter() return self.create_stack().run().get_result()
def extract_project(self): Log("Extracting project..") # Create required folders in 'extracted_songs' self.create_required_folders() # Extract cache to 'temp' folder Log("Extracting project") Tar.extract(self.get_cache_file(), self.get_temp_dir().parent) # Copy and convert from 'temp' to 'extracted_songs' if not self.move_temp_to_extracted_songs(): # If function fails, remove broken extracted project # to prevent issues trying again. Folder.delete(self.get_root_dir()) Log("Could not move project from 'temp' to 'extracted_songs'..", "warning") Log.press_enter() return False return True
def is_up_to_date(self): # This function checks to see if the locally extracted project # is the most up-to-date project Log("Checking for updates..") # If this project is no extracted, then we are not up-to-date if not self.is_local(): Log("Project is not extracted") return False ## If this project is dirty, then we are up-to-date. ## We don't want to accidentally overwrite our changes! #if self.is_dirty(): # Log("Project is dirty") # return True # If there is no remote, then it is up-to-date if not self.is_remote(): Log("Project is not remote") return True # If there IS remote but not local cache, we are NOT up-to-date if not self.is_cached(): Log("Project is not cached") return False # If we ARE extracted, AND remote, AND cached.. # Lets check if the two hashes compare local_hash = Hash.get_project_hash(self) if not local_hash: # We will need to re-download if local hash doesnt exist still # If we reach this, something went wrong and we have to re-download return False # If local and remote hashes match, we are up-to-date if local_hash == self.entry.data["hash"]: return True # default return return False
def download(ID, save_path): if Dev.get("NO_DOWNLOAD"): return True # We will attempt to download 5x before we give up i = 0 while i < 5: try: save_path = Path(save_path) request = Drive.service.files().get_media(fileId=ID) fh = io.BytesIO() downloader = MediaIoBaseDownload(fh, request, chunksize=51200*1024) print(f'----> "{save_path.name}" Downloaded 0%', end="\r", flush=True) done = False while done is False: status, done = downloader.next_chunk() if status: print(f'----> "{save_path.name}" Downloaded {int(status.progress() * 100)}%', end="\r", flush=True) # If we made it this far then we should be all set i = 99 except: i += 1 Log(f'Download attempt #{i}..',"notice") # If we got a return from the Drive AND we completed the while loop if downloader and i==99: print(f' "{save_path.name}" Downloaded successfully!') fh.seek(0) with open(save_path.absolute(), "wb") as f: shutil.copyfileobj(fh, f, length=1024*1024) return True Log(f'Failed to download "{save_path.name}"!',"warning") return False
def get_mult_choice(self, options): if isinstance(options, list): optlist = "/".join(options) ans = "" while not ans in options: ans = input(f' ({optlist}): ').lower() if not ans in options: print(f'\n "{ans}" is not a valid option!\n') Log(f'Dialog Mult Choice Answer: "{ans}"', 'sub', quiet=True) return ans
def duplicate(self): if not self.dialog_copy_confirm(): return False new_name = self.dialog_copy_new_name() new_path = self.get_root_dir().parent / new_name Log(f'Duplicating "{self.entry.name}" to "{new_name}"..') Folder.copy(self.get_root_dir(), new_path) Log(f'Renaming song file', 'sub') File.rename(new_path / (self.get_song_file().name), new_path / f'{new_name}.song') if self.get_song_file(version="original").exists(): Log(f'Renaming *original song file', 'sub') File.rename( new_path / (self.get_song_file(version="original").name), new_path / f'{new_name}_original.song') Menu.notice = f'Created Project "{new_name}"!' return True
def download_and_extract(self): if not self.download_project(): Log("There was a problem downloading the update..", "warning") Log.press_enter() return False if not self.extract_project(): Log("There was a problem extracting the project..", "warning") Log.press_enter() return False return True
def confirm(self): self.show([ f'', f'', self.create_title("Are you sure?"), f'', f'', ], clear=False) ans = self.get_mult_choice(['y', 'n']) Log(f'Dialog Confirm Answer: "{ans}"', quiet=True) if ans == 'y': return True else: return False
def is_cached(self): # If the project has a *.lof file in 'compressed_songs' directory # Must check if cache AND "hash" exist if self.get_cache_file().exists(): if not Hash.get_project_hash(self): Log(f'Project "{self.entry.name}" is cached but not hashed!', "warning") Log.press_enter() return False Log(f'Project "{self.entry.name}" is locally cached') return True else: if Hash.get_project_hash(self): Log(f'Project "{self.entry.name}" is hashed but not cached!', "warning") Log.press_enter() Log(f'Project "{self.entry.name}" is not locally cached') return False
def to_wav(self, folderpath): folderpath = Path(folderpath) Folder.create(folderpath) mp3 = self.filepath wav = Path(f'{folderpath.absolute()}/{mp3.stem}.wav') stem, num, ext = File.split_name(wav.name) if wav.is_file(): # File already exists locally, do not do anything Log(f'File "{wav.name}" already exists, keeping local file.') return True Run.ffmpeg(args="-i", source=mp3.absolute(), destination=wav.absolute(), codec="-c:a pcm_s24le") return True
def initialize(): i = 0 while i < 5: try: Drive.service = None # If modifying these scopes, delete the file token.pickle. SCOPES = ["https://www.googleapis.com/auth/drive"] # SCOPES = ["https://www.googleapis.com/auth/admin.directory.group"] creds = None if os.path.exists("token.pickle"): with open("token.pickle", "rb") as token: creds = pickle.load(token) # If there are no (valid) credentials available, let the user log in. if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file( "credentials.json", SCOPES) creds = flow.run_local_server(port=0) # Save the credentials for the next run with open("token.pickle", "wb") as token: pickle.dump(creds, token) Drive.service = build("drive", "v3", credentials=creds) # Root of the LOFSM project on the cloud Drive.root_id = Drive.get_root_id() i = 99 except: i+=1 Log(f'Attempting to connect to the drive.. #{i}',"notice") if i != 99: raise Exception("\n\n#### There was a problem connecting to Google Drive! Check your internet connection!")
def menu_open_project(filter="active"): # The filter argument will sort between categoried projects # If "all" is passed, all songs will show up # If "active" is passed, only songs that are categorized with "active" # in the "project_type" entry field will show up. Log("Gathering projects, please wait..", "notice") projects = ProjectIndex.get_all_projects() if filter != "all": projects = [ x for x in projects if x.entry.data["project_type"] == filter ] menu = Menu(title="What project would you like to select?", options=[x.create_menu_item() for x in projects]) result = menu.get_result() if result == "back": return True else: return menu_project_options(projects[result])
def extract(filepath, destination): filepath = Path(filepath) destination = Path(destination) Log(f'Extracting "{filepath.stem}"..') if not Dev.get("NO_EXTRACT"): try: os.system( f"tar -xzf {filepath.absolute()} -C {destination.absolute()}" ) except Exception as e: Log(f'Failed to extract "{filepath.stem}"') Log(f'\n\n{e}\n\n', None) Log.press_enter() exit() Log(f'Finished extracting "{filepath.stem}!"')
def open_studio_one(self): Log("OPENING STUDIO ONE","notice") # First create a temp version of the project File.recursive_overwrite( self.get_song_file(), self.get_song_file(version="temp") ) Dialog( title = "Wait for Studio One to close!", body = "DO NOT CLOSE THIS WINDOW!!" ) if Dev.get("NO_OPEN_STUDIO_ONE"): # Do not open studio one return True # Build the dummy files self.set_dummy_files() # Open Studio One ans = Run.prg( alias = "open", command = f'{ self.get_song_file(version="temp") }', wait = True ) # Remove dummy files self.remove_dummy_files() if ans != 0: return False # Copy over any saved data to the original song file File.recursive_overwrite( self.get_song_file(version="temp"), self.get_song_file() ) File.delete( self.get_song_file(version="temp") ) return True
def compress(folderpath, destination): folderpath = Path(folderpath) destination = Path(destination) cwd = Path.cwd() Log(f'Compressing "{folderpath.stem}"..') if not Dev.get("NO_COMPRESS"): try: os.system( f'cd "{folderpath.parent}" && tar -czf "{destination.absolute()}" "{folderpath.name}" && cd "{cwd.absolute()}"' ) except Exception as e: Log(f'Failed to compress "{folderpath.stem}"') Log(f'\n\n{e}\n\n', None) Log.press_enter() exit() Log(f'Finished compressing "{folderpath.stem}!"')
def is_remote(self): # If the project exists on the cloud # Must check if remote "id" AND "hash" both exist if self.entry.data["id"]: if not self.entry.data["hash"]: Log( f'Project "{self.entry.name}" has remote ID but not remote hash!', "warning") Log.press_enter() return False return True else: if self.entry.data["hash"]: Log( f'Project "{self.entry.name}" does not have remote ID but has remote cache!', "warning") Log.press_enter() return False
def run_migrations(): migrations = glob("migrations/*") migrations.sort() migrations = [Path(x) for x in migrations] current_version = Settings.get_version() result = True for migration in migrations: migration_version = Decimal(migration.stem.replace("_", ".")) if migration_version > current_version: Update.install_pip_packages() ans = Run.prg("python", migration.absolute(), useSubprocess=True) if ans == 0: Slack.upload_log() Log( f'There was a problem loading this migration file!\n "{migration.absolute()}"', "warning") Log.press_enter() # If there was an issue upgrading the migration, we don't want to set the new version return False elif ans != 1: Log("You must restart the program to finish the update!", "notice") Log.press_enter() result = False Settings.set_version(migration_version) # Push a notification to Slack Slack( f'{Settings.get_username(capitalize=True)} has upgraded to V{migration_version}!' ) return result
def upload_project(self): # Make sure our project is the most up-to-date if not self.is_up_to_date(): Log( "There are updates for this project on the cloud!.. can not upload!", "warning") Log.press_enter() return False # Make sure we set the category if it's never been uploaded before if not self.is_remote(): self.change_category(back=False) if not Dev.get("NO_OPEN_STUDIO_ONE"): if self.dialog_remove_unused_audio_files(): if not self.open_studio_one(): return False # Create folder for mixdowns on cloud if it doesnt exist if not Drive.get_id(self.entry.name): Drive.mkdir(self.entry.name) # We will need to set the local hash on our own incase of failure # to upload if not self.compress_project(set_hash=False): return False # Upload compressed project Log("Uploading.. please be patient", "notice") if not Dev.get("NO_LOF_UPLOAD"): result = Drive.upload(filepath=self.get_cache_file(), mimeType=Drive.mimeType['zip']) if not result: Log("Drive upload could not complete..", "warning") Log.press_enter() return False # Update local hash Hash.set_project_hash(self, Hash.create_hash_from_project(self)) # Update remote hash if file was successfully uploaded self.entry.data["id"] = result self.entry.data["hash"] = Hash.get_project_hash(self) self.entry.update() Slack( f'{Slack.get_nice_username()} uploaded a new version of {Slack.make_nice_project_name(self.entry.name)}' ) # Remove name from dirty list self.remove_dirty() # Since we successfully uploaded to the drive, we can now get # rid of the *original song file File.recursive_overwrite(self.get_song_file(), self.get_song_file(version="original")) Log("Compression and upload complete!", "notice") return True
def move_extracted_song_to_temp(self): # Make sure we don't have any dummy files self.remove_dummy_files() for location in ["Media", "Bounces"]: # Remove unused cached audio from location # Garbage collector for unused audio files mp3s = Folder.ls_files(self.get_temp_dir() / location, "mp3") wav_names = [ x.stem for x in Folder.ls_files(self.get_root_dir() / location, "wav") ] for mp3 in mp3s: if mp3.stem not in wav_names: File.delete(mp3) # Convert and move wavs to mp3s Log(f'Converting "{location}" wav\'s to mp3\'s') if not Audio.folder_to_mp3(self.get_root_dir() / location, self.get_temp_dir() / location): return False # Copy over dummy.json Log(f'Copying over "{location}" dummy.json') if self.get_dummy_db(location, temp=False).exists(): File.recursive_overwrite( self.get_dummy_db(location, temp=False), self.get_dummy_db(location, temp=True), ) # Upload scratch files and mixdowns to the cloud mp3s = Folder.ls_files(self.get_temp_dir() / "Media", "mp3", "Scratch*") mp3s.extend( Folder.ls_files(self.get_temp_dir() / "Media", "mp3", "SCRATCH*")) mp3s.extend(Folder.ls_files(self.get_root_dir() / "Mixdown", "mp3")) for mp3 in mp3s: mix_folder_id = Drive.get_id(self.entry.name) if not Drive.get_id(mp3.name, mix_folder_id): Log(f'Uploading "{mp3.name}" to the cloud', "sub") mp3_id = Drive.upload(filepath=mp3, mimeType=Drive.mimeType["mp3"], parent=mix_folder_id) audio_type = "Scratch" if mp3.parent.name == "Media" else "#MIXDOWN#" Slack.send_link( link_name= f'{audio_type} for {self.entry.name}, "{mp3.name}"', ID=mp3_id) else: Log(f'Audio file "{mp3.name}" already exists on the cloud!', "sub") # Copy over mixdowns to temp Log("Copying over 'Mixdown' mp3's") Folder.copy(self.get_root_dir() / "Mixdown", self.get_temp_dir() / "Mixdown") # Lastly, copy over the song file File.recursive_overwrite(self.get_song_file(temp=False), self.get_song_file(temp=True)) return True
def to_mp3(self, folderpath, username_ignore=False): folderpath = Path(folderpath) wav = self.filepath mp3 = Path(f'{folderpath.absolute()}/{wav.stem}.mp3') username = Settings.get_username() if "_old" in wav.name: Dialog( title="Warning! Can't Upload *_old* Audio File!", body=[ f'It appears that you have not yet resolved the audio file', f'conflict for "{wav.name}"! You must either delete this file,', f'or re-name it and re-link it in Studio One!', f'\n', f'\n', ], clear=False).press_enter() return False if not mp3.is_file(): if not username in wav.name.lower() and not username_ignore: dialog = Dialog( title="Warning! Where Is Your Name?!", body=[ f'It appears that the file', f'\n', f'\n', f' - {wav.parent.absolute().name}/{wav.name}', f'\n', f'\n', f'does not contain your name.. Are you sure you recorded', f'on the correct track?! Doing this can cause serious', f'version control issues!!', f'\n', f'\n', f'Since you have already removed unused audio files from', f'the pool, AND selected the checkbox to delete those', f'audio files.. You should go back into your Studio One', f'project, remove this clip from the timeline, OR rename', f'this clip to include your name, and then re-run the', f'upload!', f'\n', f'\n', f'If you are ABSOLUTELY SURE that this is in error, aka', f'uploading a project from band practice, then type "yes"', f'at the prompt, or "yesall" to ignore all other warnings', f'for this.', f'\n', f'\n', f'If you want to exit now, type "no" at the prompt.', f'\n', f'\n', ], clear=False) ans = dialog.get_mult_choice(["yes", "yesall", "no"]) if ans == "no": return False elif ans == "yesall": username_ignore = True else: Log(f'Keeping cached file "{mp3.name}"') return True Run.ffmpeg(args="-i", source=wav.absolute(), destination=mp3.absolute(), codec="") if username_ignore: return {"username_ignore": True} return True
import traceback,sys from src.Slack import Slack from src.Update import Update from src.Settings import Settings from src.TERMGUI.Log import Log from src.FileManagement.Folder import Folder from src.menus.menu_main import menu_main # MAIN FUNCTION if __name__ == '__main__': # Create file structure if it doesn't exist Folder.create("compressed_songs") Folder.create("extracted_songs") Folder.create("temp") Folder.create("templates") # Create settings file if it doesn't exist Settings.create() # Start main menu try: menu_main() except Exception as e: Log(traceback.format_exc(),"warning") # 0 = there was a problem # 1 = exit gracefully sys.exit(0)