Compare commits

...

16 Commits

Author SHA1 Message Date
Lynne Megido d27e1481c3
check against tag format type instead of file extension
sorta like, "if tag_format == 'id3'" rather than "if song_format == ['mp3', 'wav', 'aiff']"
2020-10-28 22:32:20 +10:00
Lynne Megido 3e0eb75999
remove unneeded file extension 2020-10-28 01:57:49 +10:00
Lynne Megido 92f398a326
turns out i didn't need to do anything to add alac support - they work the same as aac m4a files do. although i did find and fix a bug in the m4a handling so that's good at least 0uo 2020-10-27 00:44:40 +10:00
Lynne Megido 102edd01a3
added project files, aiff support 2020-10-26 23:55:06 +10:00
Lynne Megido c641c63fda
my py dot ini 2020-10-26 23:39:36 +10:00
Lynne Megido 16ddf2118c
wav support! 2020-10-26 23:34:04 +10:00
Lynne Megido 5d6ac79cd1
mypy strict support! 2020-10-26 23:05:18 +10:00
Lynne Megido 1804ddf457
code cleanup 2020-10-26 22:44:33 +10:00
Lynne Megido 7334f67e45
put everything in main(), zero mypy issues 2020-10-26 22:41:01 +10:00
Lynne Megido a6409c9c35
mypy integration 2020-10-26 22:35:27 +10:00
Lynne Megido 5b95996d8c
fairly major restructuring that should make future format support a lot easier, support for songs with partially or fully incomplete metadata 2020-10-17 23:54:50 +10:00
Lynne Megido f78c8d7c78
m4a support 2020-10-17 21:02:52 +10:00
Lynne Megido 7cb6545096
mp3 support! more helpful interface! better code! yahoo!! 2020-10-17 19:42:38 +10:00
Lynne Megido 124f0f7b42
flac support and an argument for choosing when to embed cover art 2020-10-17 17:26:25 +10:00
Lynne Megido 801b7369c4
cleaner code, type annotations, and it even runs (slightly) faster! =u= 2020-10-17 16:23:55 +10:00
Lynne Megido 2f62c53a9f
support for png covers with transparency, other fixes 2020-10-17 13:34:56 +10:00
16 changed files with 850 additions and 132 deletions

3
.gitignore vendored
View File

@ -1,4 +1,7 @@
!bcao.py
!requirements.txt
!.gitignore
!mypy.ini
!.run/
!.idea/
*

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

13
.idea/bcao.iml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.mypy_cache" />
<excludeFolder url="file://$MODULE_DIR$/samples" />
<excludeFolder url="file://$MODULE_DIR$/test" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8 (bcao)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -0,0 +1,3 @@
<component name="ProjectDictionaryState">
<dictionary name="lynne" />
</component>

6
.idea/discord.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
</component>
</project>

View File

@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Default" />
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (bcao)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/bcao.iml" filepath="$PROJECT_DIR$/.idea/bcao.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

328
.idea/workspace.xml Normal file
View File

@ -0,0 +1,328 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="BranchesTreeState">
<expand>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="LOCAL_ROOT" type="e8cecc67:BranchNodeDescriptor" />
</path>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
</path>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="GROUP_NODE:origin" type="e8cecc67:BranchNodeDescriptor" />
</path>
</expand>
<select />
</component>
<component name="ChangeListManager">
<list default="true" id="f581197a-f26b-4fde-b746-e72c0ed1bb2a" name="Default Changelist" comment="my py dot ini">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bcao.py" beforeDir="false" afterPath="$PROJECT_DIR$/bcao.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FlaskConsoleOptions" custom-start-script="import sys&#10;sys.path.extend([WORKING_DIR_AND_PYTHON_PATHS])&#10;from flask.cli import ScriptInfo&#10;locals().update(ScriptInfo(create_app=None).load_app().make_shell_context())&#10;print(&quot;Python %s on %s\nApp: %s [%s]\nInstance: %s&quot; % (sys.version, sys.platform, app.import_name, app.env, app.instance_path))">
<envs>
<env key="FLASK_APP" value="app" />
</envs>
<option name="myCustomStartScript" value="import sys&#10;sys.path.extend([WORKING_DIR_AND_PYTHON_PATHS])&#10;from flask.cli import ScriptInfo&#10;locals().update(ScriptInfo(create_app=None).load_app().make_shell_context())&#10;print(&quot;Python %s on %s\nApp: %s [%s]\nInstance: %s&quot; % (sys.version, sys.platform, app.import_name, app.env, app.instance_path))" />
<option name="myEnvs">
<map>
<entry key="FLASK_APP" value="app" />
</map>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
<option name="RESET_MODE" value="HARD" />
</component>
<component name="JupyterTrust" id="c9c24a84-69f9-4c1e-bffb-78383de38689" />
<component name="ProjectId" id="1iwv1rbtMpCLK7D695td98N37pr" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="com.intellij.ide.scratch.LRUPopupBuilder$1/New Scratch File" value="TEXT" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="node.js.detected.package.eslint" value="true" />
<property name="node.js.detected.package.tslint" value="true" />
<property name="node.js.path.for.package.eslint" value="project" />
<property name="node.js.path.for.package.tslint" value="project" />
<property name="node.js.selected.package.eslint" value="(autodetect)" />
<property name="node.js.selected.package.tslint" value="(autodetect)" />
<property name="settings.editor.selected.configurable" value="preferences.pluginManager" />
</component>
<component name="RunManager" selected="Python.bcao (ceres)">
<configuration default="true" type="PythonConfigurationType" factoryName="Python">
<module name="bcao" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<list>
<item itemvalue="Python.bcao (io)" />
<item itemvalue="Python.bcao (ceres)" />
<item itemvalue="Python.mypy" />
</list>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="f581197a-f26b-4fde-b746-e72c0ed1bb2a" name="Default Changelist" comment="" />
<created>1602831263913</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1602831263913</updated>
<workItem from="1602831267794" duration="18279000" />
<workItem from="1602850978698" duration="7902000" />
<workItem from="1602908398925" duration="34104000" />
<workItem from="1603714609431" duration="5637000" />
<workItem from="1603720261881" duration="6236000" />
</task>
<task id="LOCAL-00001" summary="mp3 support! more helpful interface! better code! yahoo!!">
<created>1602927759343</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1602927759343</updated>
</task>
<task id="LOCAL-00002" summary="fairly major restructuring that should make future format support a lot easier, support for songs with partially or fully incomplete metadata">
<created>1602942890966</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1602942890966</updated>
</task>
<task id="LOCAL-00003" summary="mypy integration">
<created>1603715728224</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1603715728224</updated>
</task>
<task id="LOCAL-00004" summary="put everything in main(), zero mypy issues">
<created>1603716061845</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1603716061845</updated>
</task>
<task id="LOCAL-00005" summary="code cleanup">
<created>1603716273616</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1603716273616</updated>
</task>
<task id="LOCAL-00006" summary="mypy strict support!">
<created>1603717518466</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1603717518466</updated>
</task>
<task id="LOCAL-00007" summary="wav support!">
<created>1603719245013</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1603719245013</updated>
</task>
<task id="LOCAL-00008" summary="my py dot ini">
<created>1603719576507</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1603719576507</updated>
</task>
<task id="LOCAL-00009" summary="added project files, aiff support">
<created>1603720506558</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1603720506558</updated>
</task>
<task id="LOCAL-00010" summary="turns out i didn't need to do anything to add alac support - they work the same as aac m4a files do. although i did find and fix a bug in the m4a handling so that's good at least 0uo">
<created>1603723480563</created>
<option name="number" value="00010" />
<option name="presentableId" value="LOCAL-00010" />
<option name="project" value="LOCAL" />
<updated>1603723480563</updated>
</task>
<task id="LOCAL-00011" summary="remove unneeded file extension">
<created>1603814270091</created>
<option name="number" value="00011" />
<option name="presentableId" value="LOCAL-00011" />
<option name="project" value="LOCAL" />
<updated>1603814270092</updated>
</task>
<option name="localTasksCounter" value="12" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
<option name="oldMeFiltersMigrated" value="true" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="mp3 support! more helpful interface! better code! yahoo!!" />
<MESSAGE value="fairly major restructuring that should make future format support a lot easier, support for songs with partially or fully incomplete metadata" />
<MESSAGE value="mypy integration" />
<MESSAGE value="put everything in main(), zero mypy issues" />
<MESSAGE value="code cleanup" />
<MESSAGE value="mypy strict support!" />
<MESSAGE value="wav support!" />
<MESSAGE value="my py dot ini" />
<MESSAGE value="added project files, aiff support" />
<MESSAGE value="turns out i didn't need to do anything to add alac support - they work the same as aac m4a files do. although i did find and fix a bug in the m4a handling so that's good at least 0uo" />
<MESSAGE value="remove unneeded file extension" />
<option name="LAST_COMMIT_MESSAGE" value="remove unneeded file extension" />
</component>
<component name="WindowStateProjectService">
<state x="555" y="188" width="800" height="672" key="#Deployment" timestamp="1602927147820">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="555" y="188" width="800" height="672" key="#Deployment/0.0.1920.1055@0.0.1920.1055" timestamp="1602927147820" />
<state x="811" y="199" width="732" height="632" key="#Inspections" timestamp="1602832260834">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="811" y="199" width="732" height="632" key="#Inspections/0.0.1920.1055@0.0.1920.1055" timestamp="1602832260834" />
<state x="697" y="368" width="516" height="313" key="#Notifications" timestamp="1602831592098">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="697" y="368" width="516" height="313" key="#Notifications/0.0.1920.1055@0.0.1920.1055" timestamp="1602831592098" />
<state x="555" y="170" width="800" height="706" key="#Plugins" timestamp="1603714662919">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state x="555" y="170" width="800" height="706" key="#Plugins/0.0.1920.1054@0.0.1920.1054" timestamp="1603714662919" />
<state x="719" y="227" key="#Python" timestamp="1603720399983">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state x="719" y="227" key="#Python/0.0.1920.1054@0.0.1920.1054" timestamp="1603720399983" />
<state x="418" y="185" width="1084" height="709" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1603723412351">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state x="418" y="185" width="1084" height="709" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.0.1920.1054@0.0.1920.1054" timestamp="1603723412351" />
<state x="418" y="185" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.0.1920.1055@0.0.1920.1055" timestamp="1602931259631" />
<state x="888" y="193" width="424" height="721" key="#com.intellij.ide.macro.MacrosDialog" timestamp="1602931237546">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="888" y="193" width="424" height="721" key="#com.intellij.ide.macro.MacrosDialog/0.0.1920.1055@0.0.1920.1055" timestamp="1602931237546" />
<state x="707" y="363" width="797" height="527" key="#com.intellij.tools.ToolEditorDialog" timestamp="1602908296483">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="707" y="363" width="797" height="527" key="#com.intellij.tools.ToolEditorDialog/0.0.1920.1055@0.0.1920.1055" timestamp="1602908296483" />
<state x="549" y="98" width="1059" height="853" key="CommitChangelistDialog2" timestamp="1602927110754">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="549" y="98" width="1059" height="853" key="CommitChangelistDialog2/0.0.1920.1055@0.0.1920.1055" timestamp="1602927110754" />
<state x="100" y="99" width="1720" height="856" key="DiffContextDialog" timestamp="1603814233028">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state x="100" y="99" width="1720" height="856" key="DiffContextDialog/0.0.1920.1054@0.0.1920.1054" timestamp="1603814233028" />
<state x="100" y="99" width="1720" height="856" key="DiffContextDialog/0.0.1920.1055@0.0.1920.1055" timestamp="1602915909590" />
<state x="743" y="285" width="424" height="479" key="FileChooserDialogImpl" timestamp="1602850965686">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="743" y="285" width="424" height="479" key="FileChooserDialogImpl/0.0.1920.1055@0.0.1920.1055" timestamp="1602850965686" />
<state width="1878" height="281" key="GridCell.Tab.0.bottom" timestamp="1603723362538">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state width="1878" height="281" key="GridCell.Tab.0.bottom/0.0.1920.1054@0.0.1920.1054" timestamp="1603723362538" />
<state width="1878" height="282" key="GridCell.Tab.0.bottom/0.0.1920.1055@0.0.1920.1055" timestamp="1602942953878" />
<state width="1878" height="281" key="GridCell.Tab.0.center" timestamp="1603723362538">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state width="1878" height="281" key="GridCell.Tab.0.center/0.0.1920.1054@0.0.1920.1054" timestamp="1603723362538" />
<state width="1878" height="282" key="GridCell.Tab.0.center/0.0.1920.1055@0.0.1920.1055" timestamp="1602942953877" />
<state width="1878" height="281" key="GridCell.Tab.0.left" timestamp="1603723362537">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state width="1878" height="281" key="GridCell.Tab.0.left/0.0.1920.1054@0.0.1920.1054" timestamp="1603723362537" />
<state width="1878" height="282" key="GridCell.Tab.0.left/0.0.1920.1055@0.0.1920.1055" timestamp="1602942953877" />
<state width="1878" height="281" key="GridCell.Tab.0.right" timestamp="1603723362538">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state width="1878" height="281" key="GridCell.Tab.0.right/0.0.1920.1054@0.0.1920.1054" timestamp="1603723362538" />
<state width="1878" height="282" key="GridCell.Tab.0.right/0.0.1920.1055@0.0.1920.1055" timestamp="1602942953877" />
<state width="1878" height="347" key="GridCell.Tab.1.bottom" timestamp="1603723347647">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state width="1878" height="347" key="GridCell.Tab.1.bottom/0.0.1920.1054@0.0.1920.1054" timestamp="1603723347647" />
<state width="1878" height="347" key="GridCell.Tab.1.center" timestamp="1603723347647">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state width="1878" height="347" key="GridCell.Tab.1.center/0.0.1920.1054@0.0.1920.1054" timestamp="1603723347647" />
<state width="1878" height="347" key="GridCell.Tab.1.left" timestamp="1603723347647">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state width="1878" height="347" key="GridCell.Tab.1.left/0.0.1920.1054@0.0.1920.1054" timestamp="1603723347647" />
<state width="1878" height="347" key="GridCell.Tab.1.right" timestamp="1603723347647">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state width="1878" height="347" key="GridCell.Tab.1.right/0.0.1920.1054@0.0.1920.1054" timestamp="1603723347647" />
<state x="182" y="88" width="1536" height="869" key="MergeDialog" timestamp="1602851077617">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="182" y="88" width="1536" height="869" key="MergeDialog/0.0.1920.1055@0.0.1920.1055" timestamp="1602851077617" />
<state x="596" y="306" width="718" height="437" key="MultipleFileMergeDialog" timestamp="1602851077619">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="596" y="306" width="718" height="437" key="MultipleFileMergeDialog/0.0.1920.1055@0.0.1920.1055" timestamp="1602851077619" />
<state x="334" y="44" width="1315" height="941" key="SettingsEditor" timestamp="1603776383135">
<screen x="0" y="0" width="1920" height="1054" />
</state>
<state x="334" y="44" key="SettingsEditor/0.0.1920.1054@0.0.1920.1054" timestamp="1603776383135" />
<state x="334" y="44" width="1315" height="941" key="SettingsEditor/0.0.1920.1055@0.0.1920.1055" timestamp="1602927410438" />
<state x="100" y="99" width="1720" height="856" key="com.intellij.history.integration.ui.views.FileHistoryDialog" timestamp="1602937117208">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="100" y="99" width="1720" height="856" key="com.intellij.history.integration.ui.views.FileHistoryDialog/0.0.1920.1055@0.0.1920.1055" timestamp="1602937117208" />
<state x="623" y="232" width="672" height="678" key="search.everywhere.popup" timestamp="1602936893566">
<screen x="0" y="0" width="1920" height="1055" />
</state>
<state x="623" y="232" width="672" height="678" key="search.everywhere.popup/0.0.1920.1055@0.0.1920.1055" timestamp="1602936893566" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/bcao$mypy.coverage" NAME="mypy Coverage Results" MODIFIED="1603717428705" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/bcao$bcao.coverage" NAME="bcao Coverage Results" MODIFIED="1603719196915" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/bcao$bcao__ceres_.coverage" NAME="bcao (ceres) Coverage Results" MODIFIED="1603723362525" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
</component>
</project>

24
.run/bcao (ceres).run.xml Normal file
View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="bcao (ceres)" type="PythonConfigurationType" factoryName="Python">
<module name="bcao" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="$PROJECT_DIR$/venv/bin/python" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/bcao.py" />
<option name="PARAMETERS" value="&quot;A Cerulean State - As if I remembered something.zip&quot; -d &quot;/tmp/out/&quot;" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

24
.run/bcao.run.xml Normal file
View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="bcao (io)" type="PythonConfigurationType" factoryName="Python">
<module name="bcao" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="$PROJECT_DIR$/venv/bin/python" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/bcao.py" />
<option name="PARAMETERS" value="&quot;Braxton Burks - Time &amp; Space.zip&quot; -d &quot;$USER_HOME$/Documents&quot;" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

24
.run/mypy.run.xml Normal file
View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="mypy" type="PythonConfigurationType" factoryName="Python">
<module name="bcao" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/venv/bin/mypy" />
<option name="PARAMETERS" value="bcao.py" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

511
bcao.py
View File

@ -6,162 +6,409 @@
# output: it organises it, adds cover art, puts it in the right place...
import argparse
import base64
import io
import os
import re
import subprocess
import sys
import tempfile
import shutil
from zipfile import ZipFile, BadZipFile
from os import path
from base64 import b64encode
from zipfile import ZipFile
from pathlib import Path
from typing import Optional, Union, List, Dict
# pycharm tells me some of these classes shouldn't be imported because they're not declared in __all__.
# however, the mutagen docs show example code where someone creates a mutagen.flac.Picture by referring to it as
# Picture(), implying that they had imported mutagen.flac.Picture, and therefore i'm right and the computer is WRONG
# https://mutagen.readthedocs.io/en/latest/api/flac.html#mutagen.Picture.data
import mutagen
from mutagen.flac import Picture
from mutagen import id3
# noinspection PyProtectedMember
from mutagen.flac import Picture, FLAC
from mutagen.oggvorbis import OggVorbis
from mutagen.mp3 import MP3
from mutagen.mp4 import MP4, MP4Cover
# noinspection PyProtectedMember
from mutagen.id3 import APIC, PictureType, Frame, TRCK, TPE1, TIT2, TALB, TPE2
from PIL import Image
def log(message: str, importance: int = 0):
fully_supported: List[str] = ["ogg", "flac", "mp3", "m4a", "wav", "aiff"]
MutagenFile = Union[MP3, FLAC, OggVorbis, mutagen.FileType]
MutagenTags = Union[mutagen.id3.ID3Tags, mutagen.mp4.Tags, mutagen.oggvorbis.OggVCommentDict]
args: argparse.Namespace
tmp_dir: tempfile.TemporaryDirectory # type: ignore
format_lookup: Dict[str, str] = {
"mp3": "id3",
"m4a": "m4a",
"ogg": "vorbis",
"flac": "vorbis",
"wav": "id3",
"aiff": "id3"
}
class SongInfo:
tag_lookup: Dict[str, Dict[str, str]] = {
"track": {"id3": "TRCK", "m4a": "trkn", "vorbis": "tracknumber"},
"artist": {"id3": "TPE1", "m4a": "©ART", "vorbis": "artist"},
"title": {"id3": "TIT2", "m4a": "©nam", "vorbis": "title"},
"album": {"id3": "TALB", "m4a": "©alb", "vorbis": "album"},
"album_artist": {"id3": "TPE2", "m4a": "aART", "vorbis": "albumartist"}
}
def __init__(self, file_name: Path):
self.m_file: MutagenFile = mutagen.File(file_name)
self.m_tags: MutagenTags = self.m_file.tags
self.file_name = str(file_name.name)
self.format = path.splitext(file_name)[1][1:]
self.fallback = False
if self.format not in format_lookup:
raise ValueError(f"Unsupported file type: {self.format}")
fallbacks = re.match(
r"^(?P<artist>.+) - (?P<album>.+) - (?P<track>\d{2,}) (?P<title>.+)\.(?:ogg|flac|aiff|wav|mp3|m4a)$",
self.file_name
)
if fallbacks is None:
die("Couldn't determine fallback tags!")
return # needed for mypy
# set default values for the tags, in case the file is missing any (or all!) of them
self.tags: Dict[str, str] = {
"track": str(int(fallbacks.group("track"))), # convert to int and str again to turn e.g. "01" into "1"
"artist": fallbacks.group("artist"),
"title": fallbacks.group("title"),
"album": fallbacks.group("album"),
"album_artist": fallbacks.group("artist")
}
# set list_tags to the default tags in list form
# i.e. for every tag, set list_tags[x] = [tags[x]]
self.list_tags: Dict[str, List[str]] = dict((x[0], [x[1]]) for x in self.tags.items())
if self.m_tags is None:
# file has no tags
# generate empty tags
self.m_file.add_tags()
self.m_tags = self.m_file.tags
self.fallback = True
# write fallback tags to file
for standard_name, tag_set in self.tag_lookup.items():
tag = tag_set[format_lookup[self.format]]
self.m_tags[tag] = self.new_id3_tag(standard_name, self.tags[standard_name])
self.m_file.save()
else:
for standard_name, tag_set in self.tag_lookup.items():
tag = tag_set[format_lookup[self.format]]
if tag not in self.m_tags:
print(f"{tag} not in self.m_tags")
self.fallback = True
continue
value_list = self.m_tags[tag]
if self.format == "m4a" and standard_name == "track":
# every tag in the MP4 file (from what i can tell) is a list
# this includes the track number tag, which is a tuple of ints in a list.
# because every other format is either a non-list, or a list of non-lists, we need to account for this case
# (a list of lists of non-lists) specially, by turning it into a list of non-lists.
value_list = value_list[0]
if not isinstance(value_list, (list, tuple)):
value_list = [value_list]
# convert the list of strings/ID3 frames/ints/whatevers to sanitised strings
value_list = [sanitise(str(val)) for val in value_list]
self.tags[standard_name] = value_list[0]
self.list_tags[standard_name] = value_list
@staticmethod
def new_id3_tag(tag: str, value: str) -> Frame:
if tag == "track":
return TRCK(encoding=3, text=value)
elif tag == "artist":
return TPE1(encoding=3, text=value)
elif tag == "title":
return TIT2(encoding=3, text=value)
elif tag == "album":
return TALB(encoding=3, text=value)
elif tag == "album_artist":
return TPE2(encoding=3, text=value)
else:
raise ValueError(f"Unknown tag type {tag}!")
def get_target_name(self, zeroes: int) -> str:
return f"{self.tags['track'].zfill(zeroes)} {self.tags['title']}.{self.format}"
def has_cover(self) -> bool:
if self.format == "flac":
# needs to be handled separately from ogg, as it doesn't use the vorbis tags for cover art for whatever reason
return len(self.m_file.pictures) != 0
if format_lookup[self.format] == "vorbis":
return "metadata_block_picture" in self.m_tags and len(self.m_tags["metadata_block_picture"]) != 0
if format_lookup[self.format] == "id3":
apics: List[APIC] = self.m_tags.getall("APIC")
for apic in apics:
if apic.type == PictureType.COVER_FRONT:
return True
return False
if format_lookup[self.format] == "m4a":
return 'covr' in self.m_tags and len(self.m_tags['covr']) != 0
raise NotImplementedError("Song format not yet implemented.")
def set_cover(self, to_embed: Union[Picture, APIC, MP4Cover]) -> None:
# embed cover art
if self.format == "flac":
self.m_file.clear_pictures()
self.m_file.add_picture(to_embed)
elif format_lookup[self.format] == "vorbis":
self.m_tags["metadata_block_picture"] = [b64encode(to_embed.write()).decode("ascii")]
elif format_lookup[self.format] == "id3":
self.m_tags.add(to_embed)
elif format_lookup[self.format] == "m4a":
self.m_tags['covr'] = [to_embed]
self.m_file.save()
def __getitem__(self, item: str) -> str:
return self.tags[item]
def log(message: str, importance: int = 0) -> None:
if not args.quiet or importance > 0:
print(message)
def die(message: str, code: int = 1):
def die(message: str, code: int = 1) -> None:
print(message)
exit(code)
if tmp_dir is not None:
tmp_dir.cleanup()
def get_tag(m: mutagen.FileType, tag: str):
if tag == "title":
return sanitise(m['title'][0])
elif tag == "track":
return int(m['tracknumber'][0])
elif tag == "album":
return sanitise(m['album'][0])
else:
# may as well try
return sanitise(m[tag])
sys.exit(code)
def sanitise(input: str):
def sanitise(in_str: str) -> str:
if args.sanitise:
return re.sub(r"[?\\/:|*\"<>]", "_", input)
else:
return input
parser = argparse.ArgumentParser(description="Extracts the given zip file downloaded from Bandcamp and organises it.")
parser.add_argument('zip', help='The zip file to use')
parser.add_argument('-d', '--destination', dest='destination', default='/home/lynne/Music/Music/',
help="The directory to organise the music into. Default: /home/lynne/Music/Music/")
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
help='Disable non-error output and assume default artist name.')
parser.add_argument('-u', '--unsanitised', dest='sanitise', action='store_false',
help="Don't replace NTFS-unsafe characters with underscores. Not recommended.")
parser.add_argument('-t', '--threshold', dest='threshold', nargs=1, default=300,
help="Maximum acceptable cover art file size in kilobytes. Default: 300")
args = parser.parse_args()
if not os.path.exists(args.zip):
die(f"Couldn't find {args.zip}.", 2)
log("Extracting...")
tmp = tempfile.TemporaryDirectory()
cover = None
song_names = []
with ZipFile(args.zip, 'r') as zip_file:
for file in zip_file.namelist():
if re.match(r"^(.+ - ){2}\d{2,} .+\.(ogg|flac|alac|aiff|wav|mp3|opus|m4a|aac|oga)$", file):
# bandcamp zips contains songs with names formatted like "Album - Artist - 01 Song.mp3"
# for example, "King Crimson - In the Wake of Poseidon - 02 Pictures of a City.ogg"
# this regex should match only on those, and cut out (hopefully) all of the bonus material stuff, which shouldn't
# be added to the music folder (since i sync that to my phone, and "making of" videos are cool, but i don't
# have the space for it).
song_names.append(file)
zip_file.extract(file, tmp)
elif cover is None and re.match(r"cover\.(jpe?g|png)", file):
cover = file
zip_file.extract(file, tmp)
# save the format of the songs (ogg, mp3, etc)
# we'll need this to know what metadata format we should write
song_format = os.path.splitext(song_names[0])[1][1:]
log("Resizing album art to embed in songs...")
with Image.open(os.path.join(tmp, cover)) as image:
temp_cover = os.path.join(tmp, "cover-lq.jpg")
image.save(temp_cover, quality=85, optimize=True)
image_smol = image
# keep shrinking the image by 90% until it's less than {args.threshold} kilobytes
while os.path.getsize(temp_cover) / 1024 > args.threshold:
image_smol = image_smol.resize((round(image_smol.size[0] * 0.9), round(image_smol.size[1] * 0.9)))
image_smol.save(temp_cover, quality=85, optimize=True)
if image_smol.size[0] == 10:
die("Failed to resize image")
return re.sub(r"[?\\/:|*\"<>]", "_", in_str)
return in_str
# read the image file to get the file's raw data
with open(temp_cover, 'r+b') as cover_file:
data = cover_file.read()
def main() -> None:
global args, tmp_dir
with Image.open(temp_cover) as image:
if song_format == "ogg":
# i hate this
cover = Picture()
cover.data = data
cover.type = mutagen.id3.PictureType.COVER_FRONT
cover.mime = "image/jpeg"
cover.width = image.size[0]
cover.height = image.size[1]
cover.depth = image.bits
cover = base64.b64encode(cover.write()).decode("ascii")
else:
# noinspection PyTypeChecker
parser = argparse.ArgumentParser(usage='%(prog)s zip [options]',
formatter_class=argparse.RawTextHelpFormatter,
description="Extracts the given zip file downloaded from Bandcamp and organises it.",
epilog=f"Cover art can only be embedded in files of the following types: {', '.join(fully_supported).upper()}.\n"
"If the song is in any other format, %(prog)s will behave as though you passed '-c n', "
"but will otherwise work normally.\nIf the song files contain no metadata, %(prog)s will attempt "
"to parse the song's filenames to retrieve the artist, album, title, and track number.")
parser.add_argument('zip', help='The zip file to use.')
parser.add_argument('-c', '--add-cover-images', dest='process_cover', default='w', choices=['n', 'a', 'w'],
help="When to embed cover art into songs.\nOptions: [n]ever, [a]lways, [w]hen necessary.\nDefault: %(default)s")
parser.add_argument('-d', '--destination', dest='destination', default='/home/lynne/Music/Music/',
help="The directory to organise the music into.\nDefault: %(default)s")
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
help='Disable non-error output and assume default artist name.')
parser.add_argument('-u', '--unsanitised', dest='sanitise', action='store_false',
help="Don't replace NTFS-unsafe characters with underscores. Not recommended.")
parser.add_argument('-t', '--threshold', dest='threshold', nargs=1, default=300,
help="Maximum acceptable file size for cover art, in kilobytes.\nDefault: %(default)s")
args = parser.parse_args()
# convert args.threshold to bytes
args.threshold *= 1024
if not path.exists(args.zip):
die(f"Couldn't find {args.zip}.", 2)
log("Extracting...")
tmp_dir = tempfile.TemporaryDirectory()
tmp: str = tmp_dir.name
cover: Optional[str] = None
song_names: List[str] = []
with ZipFile(args.zip, 'r') as zip_file:
for file in zip_file.namelist():
if re.match(r"^(.+ - ){2}\d{2,} .+\.(ogg|flac|aiff|wav|mp3|m4a)$", file):
# bandcamp zips contains songs with names formatted like "Album - Artist - 01 Song.mp3"
# for example, "King Crimson - In the Wake of Poseidon - 02 Pictures of a City.ogg"
# this regex should match only on those, and cut out (hopefully) all of the bonus material stuff, which shouldn't
# be added to the music folder (since i sync that to my phone, and "making of" videos are cool, but i don't
# have the space for it).
song_names.append(file)
zip_file.extract(file, tmp)
elif cover is None and re.match(r"cover\.(jpe?g|png)", file):
cover = file
zip_file.extract(file, tmp)
# save the format of the songs (ogg, mp3, etc)
# we'll need this to know what metadata format we should write
song_format: str = path.splitext(song_names[0])[1][1:]
if song_format not in fully_supported:
log(f"Format {song_format} is not fully supported - cover images will not be modified", 1)
args.process_cover = 'n'
artists = []
album = None
songs = {}
zeroes = min(len(song_names), 2)
for song in song_names:
ext = os.path.splitext(song)[1:]
m = mutagen.File(os.path.join(tmp, song))
# add the song's artist to the list, if it hasn't been seen yet
[artists.append(sanitise(artist)) for artist in m['artist'] if artist not in artists]
songs[song] = f"{str(get_tag(m, 'track')).zfill(zeroes)} {get_tag(m, 'title')}.{song_format}"
album = get_tag(m, "album")
# embed cover art
if song_format == "ogg":
m["metadata_block_picture"] = [cover]
m.save()
if cover is None:
die("Unable to find cover image!")
return # needed for mypy
if len(artists) > 1 and "Various Artists" not in artists:
artists.append("Various Artists")
if args.process_cover != 'n':
log("Resizing album art to embed in songs...")
with Image.open(str(Path(tmp, cover))) as image:
temp_cover: Path = Path(tmp, "cover-lq.jpg")
artist = None
while artist is None:
log("Artist directory:")
for i in range(len(artists)):
log(f"{i+1}) {artists[i]}")
log(f"{len(artists) + 1}) Custom...")
choice = "1" if args.quiet else input("> ")
if choice.isdecimal():
if int(choice) == len(artists) + 1:
log("Enter the name to use.")
if image.mode in ["RGBA", "P"]:
# remove alpha channel
image = image.convert("RGB")
image.save(temp_cover, quality=85, optimize=True)
image_smol = image
while path.getsize(temp_cover) > args.threshold:
# keep shrinking the image by 90% until it's less than {args.threshold} kilobytes
ratio = 0.9
if path.getsize(temp_cover) > args.threshold * 2:
# if the file size of the cover is more than double the threshold, resize the cover image size by 80% instead
ratio = 0.8
image_smol = image_smol.resize([round(n * ratio) for n in image_smol.size])
image_smol.save(temp_cover, quality=85, optimize=True)
if image_smol.size[0] == 10:
# something very bad has happened here
die("Failed to resize image")
# read the image file to get its raw data
with open(temp_cover, 'r+b') as cover_file:
data = cover_file.read()
# it's really strange that the more annoying the file's metadata is, the *less* annoying it is to create cover art
# for it in mutagen.
# vorbis: open standard, so easy to use that mutagen supplies a bunch of "easy" wrappers around other tag formats to
# make them work more like vorbis comments.
# cover-annoy-o-meter: high. mutagen requires you to specify the width, height, colour depth, etc etc
# id3: well documented, but rather cryptic (which is more understandable, "album_artist" or "TPE2"?).
# cover-annoy-o-meter: not bad at all - at least you get a constructor this time - although it is kinda annoying
# that you have to specify the file encoding, and how you need both a type and a desc.
# m4a: scarce documentation, closed format, half reverse engineered from whatever itunes is doing, exists pretty
# much exclusively in the realm of apple stuff.
# cover-annoy-o-meter: all you need is the file data and the format type.
if format_lookup[song_format] == "vorbis":
# i hate this
with Image.open(io.BytesIO(data)) as image:
embed_cover = Picture()
embed_cover.data = data
embed_cover.type = PictureType.COVER_FRONT
embed_cover.mime = "image/jpeg"
embed_cover.width = image.size[0]
embed_cover.height = image.size[1]
embed_cover.depth = image.bits
elif format_lookup[song_format] == "id3":
# apparently APIC files get compressed on save if they are "large":
# https://mutagen.readthedocs.io/en/latest/api/id3_frames.html#mutagen.id3.APIC
# i don't know what that means (lossless text compression? automatic JPEG conversion?) and i don't know if or how
# i can disable it, which kinda sucks...
# if, for example, mutagen's threshold for "large" is 200KiB, then any file over that size would be reduced to
# below it, either by resizing or JPEG quality reduction or whatever, making the -t flag useless for values above
# 200 when saving MP3 files.
# the most i can tell is that mutagen uses zlib compression in some way or another for reading ID3 tags:
# https://github.com/quodlibet/mutagen/blob/release-1.45.1/mutagen/id3/_frames.py#L265
# however, it seems not to use zlib when *writing* tags, citing itunes incompatibility, in particular with APIC:
# https://github.com/quodlibet/mutagen/blob/release-1.45.1/mutagen/id3/_tags.py#L510
# given that this is the only reference to compression that i could find in the source code, and it says that
# ID3v2 compression was disabled for itunes compatibility, i'm going to assume/hope it doesn't do anything weird.
# it's worth noting that mutagen has no dependencies outside of python's stdlib, which (currently) doesn't contain
# any method for JPEG compression, so i'm 99% sure the files won't be mangled.
embed_cover = APIC(
encoding=3, # utf-8
mime="image/jpeg",
type=PictureType.COVER_FRONT,
desc='cover',
data=data
)
elif format_lookup[song_format] == "m4a":
embed_cover = MP4Cover(
data=data,
imageformat=MP4Cover.FORMAT_JPEG
)
artists: List[str] = []
album: str = "Unknown album" # it SHOULD be impossible for this value to ever appear
songs: Dict[str, str] = {}
zeroes = min(len(song_names), 2)
first_loop: bool = True
for song_name in song_names:
song = SongInfo(Path(tmp, song_name))
if first_loop:
# the first item in the artists list should be the album artist
artists.append(song["album_artist"])
album = song["album"]
first_loop = False
# add the song's artist(s) to the list
map(artists.append, song.list_tags["artist"])
songs[song_name] = song.get_target_name(zeroes)
if args.process_cover == 'a' or (args.process_cover == 'w' and song.has_cover() is False):
song.set_cover(embed_cover)
# remove duplicate artists
artists = list(dict.fromkeys(artists))
if len(artists) > 1 and "Various Artists" not in artists:
artists.append("Various Artists")
artist: Optional[str] = None
while artist is None:
log("Artist directory:")
for i, artist_name in enumerate(artists):
log(f"{i+1}) {artist_name}")
log(f"{len(artists) + 1}) Custom...")
user_choice: str = "1" if args.quiet else input("> ")
if user_choice.isdecimal():
choice: int = int(user_choice)
if choice == len(artists) + 1:
log("Enter the name to use:")
artist = input("> ")
else:
try:
artist = artists[choice - 1]
except KeyError:
log(f"Please choose a number between 1 and {len(artists) + 1}.")
else:
try:
artist = artists[int(choice) - 1]
except KeyError:
log(f"Please choose a number between 1 and {len(artists) + 1}.")
else:
log(f"Please choose a number between 1 and {len(artists) + 1}")
log(f"Please choose a number between 1 and {len(artists) + 1}")
destination = os.path.join(args.destination, artist, album)
log(f"Moving files to {destination}...")
os.makedirs(destination, exist_ok = True)
for source_name, dest_name in songs.items():
shutil.move(os.path.join(tmp, source_name), os.path.join(destination, dest_name))
shutil.move(os.path.join(tmp, "cover.jpg"), os.path.join(destination, "cover.jpg"))
destination: Path = Path(args.destination, artist, album)
log(f"Moving files to \"{destination}\"...")
os.makedirs(destination, exist_ok=True)
tmp.cleanup()
log("Done!")
for source_name, dest_name in songs.items():
shutil.move(str(Path(tmp, source_name)), str(Path(destination, dest_name)))
shutil.move(str(Path(tmp, cover)), str(Path(destination, cover)))
tmp_dir.cleanup()
log("Done!")
if __name__ == "__main__":
main()

8
mypy.ini Normal file
View File

@ -0,0 +1,8 @@
[mypy]
strict = True
[mypy-mutagen.*]
ignore_missing_imports = True
[mypy-PIL.*]
ignore_missing_imports = True