Compare commits
No commits in common. "e721a85fd2087c281eeb169a88b994872e8358c2" and "d27e1481c33f8d1ac375e547ebe167256b3d3506" have entirely different histories.
e721a85fd2
...
d27e1481c3
16 changed files with 295 additions and 401 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,14 +1,7 @@
|
||||||
*
|
!bcao.py
|
||||||
!*/
|
|
||||||
__pycache__/**
|
|
||||||
!bcao/*.py
|
|
||||||
!requirements.txt
|
!requirements.txt
|
||||||
!poetry.lock
|
|
||||||
!pyproject.toml
|
|
||||||
!bcao.pex
|
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!mypy.ini
|
!mypy.ini
|
||||||
!.run/
|
!.run/
|
||||||
!.idea/
|
!.idea/
|
||||||
!README.md
|
*
|
||||||
!build.sh
|
|
|
@ -7,7 +7,7 @@
|
||||||
<excludeFolder url="file://$MODULE_DIR$/test" />
|
<excludeFolder url="file://$MODULE_DIR$/test" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.9 (bcao)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.8 (bcao)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (bcao)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (bcao)" project-jdk-type="Python SDK" />
|
||||||
</project>
|
</project>
|
|
@ -20,27 +20,14 @@
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="f581197a-f26b-4fde-b746-e72c0ed1bb2a" name="Default Changelist" comment="my py dot ini">
|
<list default="true" id="f581197a-f26b-4fde-b746-e72c0ed1bb2a" name="Default Changelist" comment="my py dot ini">
|
||||||
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/bcao.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/bcao.iml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/bcao/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/bcao/__init__.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/bcao.py" beforeDir="false" afterPath="$PROJECT_DIR$/bcao.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/bcao/__main__.py" beforeDir="false" afterPath="$PROJECT_DIR$/bcao/__main__.py" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/bcao/song_info.py" beforeDir="false" afterPath="$PROJECT_DIR$/bcao/song_info.py" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/requirements.txt" beforeDir="false" afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="FileTemplateManagerImpl">
|
|
||||||
<option name="RECENT_TEMPLATES">
|
|
||||||
<list>
|
|
||||||
<option value="Python Script" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="FlaskConsoleOptions" custom-start-script="import sys sys.path.extend([WORKING_DIR_AND_PYTHON_PATHS]) from flask.cli import ScriptInfo locals().update(ScriptInfo(create_app=None).load_app().make_shell_context()) print("Python %s on %s\nApp: %s [%s]\nInstance: %s" % (sys.version, sys.platform, app.import_name, app.env, app.instance_path))">
|
<component name="FlaskConsoleOptions" custom-start-script="import sys sys.path.extend([WORKING_DIR_AND_PYTHON_PATHS]) from flask.cli import ScriptInfo locals().update(ScriptInfo(create_app=None).load_app().make_shell_context()) print("Python %s on %s\nApp: %s [%s]\nInstance: %s" % (sys.version, sys.platform, app.import_name, app.env, app.instance_path))">
|
||||||
<envs>
|
<envs>
|
||||||
<env key="FLASK_APP" value="app" />
|
<env key="FLASK_APP" value="app" />
|
||||||
|
@ -56,51 +43,25 @@
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
<option name="RESET_MODE" value="HARD" />
|
<option name="RESET_MODE" value="HARD" />
|
||||||
</component>
|
</component>
|
||||||
<component name="GitSEFilterConfiguration">
|
|
||||||
<file-type-list>
|
|
||||||
<filtered-out-file-type name="LOCAL_BRANCH" />
|
|
||||||
<filtered-out-file-type name="REMOTE_BRANCH" />
|
|
||||||
<filtered-out-file-type name="TAG" />
|
|
||||||
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
|
|
||||||
</file-type-list>
|
|
||||||
</component>
|
|
||||||
<component name="JupyterTrust" id="c9c24a84-69f9-4c1e-bffb-78383de38689" />
|
<component name="JupyterTrust" id="c9c24a84-69f9-4c1e-bffb-78383de38689" />
|
||||||
<component name="ProjectId" id="1iwv1rbtMpCLK7D695td98N37pr" />
|
<component name="ProjectId" id="1iwv1rbtMpCLK7D695td98N37pr" />
|
||||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
|
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
|
||||||
<ConfirmationsSetting value="2" id="Add" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectViewState">
|
<component name="ProjectViewState">
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">
|
<component name="PropertiesComponent">
|
||||||
<property name="ASKED_ADD_EXTERNAL_FILES" value="true" />
|
|
||||||
<property name="ASKED_MARK_IGNORED_FILES_AS_EXCLUDED" value="true" />
|
|
||||||
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
|
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
|
||||||
<property name="WebServerToolWindowFactoryState" value="false" />
|
<property name="WebServerToolWindowFactoryState" value="false" />
|
||||||
<property name="com.intellij.ide.scratch.LRUPopupBuilder$1/New Scratch File" value="TEXT" />
|
<property name="com.intellij.ide.scratch.LRUPopupBuilder$1/New Scratch File" value="TEXT" />
|
||||||
<property name="last_opened_file_path" value="$USER_HOME$/.local/bin/mypy" />
|
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
||||||
<property name="node.js.detected.package.eslint" value="true" />
|
<property name="node.js.detected.package.eslint" value="true" />
|
||||||
<property name="node.js.detected.package.tslint" 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.eslint" value="project" />
|
||||||
<property name="node.js.path.for.package.tslint" 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.eslint" value="(autodetect)" />
|
||||||
<property name="node.js.selected.package.tslint" value="(autodetect)" />
|
<property name="node.js.selected.package.tslint" value="(autodetect)" />
|
||||||
<property name="settings.editor.selected.configurable" value="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" />
|
<property name="settings.editor.selected.configurable" value="preferences.pluginManager" />
|
||||||
</component>
|
|
||||||
<component name="PyConsoleOptionsProvider">
|
|
||||||
<option name="myPythonConsoleState">
|
|
||||||
<console-settings module-name="bcao" is-module-sdk="true">
|
|
||||||
<option name="myUseModuleSdk" value="true" />
|
|
||||||
<option name="myModuleName" value="bcao" />
|
|
||||||
</console-settings>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="RecentsManager">
|
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
|
||||||
<recent name="$PROJECT_DIR$/test" />
|
|
||||||
<recent name="$PROJECT_DIR$/bcao" />
|
|
||||||
</key>
|
|
||||||
</component>
|
</component>
|
||||||
<component name="RunManager" selected="Python.bcao (ceres)">
|
<component name="RunManager" selected="Python.bcao (ceres)">
|
||||||
<configuration default="true" type="PythonConfigurationType" factoryName="Python">
|
<configuration default="true" type="PythonConfigurationType" factoryName="Python">
|
||||||
|
@ -126,8 +87,8 @@
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
<list>
|
<list>
|
||||||
<item itemvalue="Python.bcao (ceres)" />
|
|
||||||
<item itemvalue="Python.bcao (io)" />
|
<item itemvalue="Python.bcao (io)" />
|
||||||
|
<item itemvalue="Python.bcao (ceres)" />
|
||||||
<item itemvalue="Python.mypy" />
|
<item itemvalue="Python.mypy" />
|
||||||
</list>
|
</list>
|
||||||
</component>
|
</component>
|
||||||
|
@ -143,9 +104,7 @@
|
||||||
<workItem from="1602850978698" duration="7902000" />
|
<workItem from="1602850978698" duration="7902000" />
|
||||||
<workItem from="1602908398925" duration="34104000" />
|
<workItem from="1602908398925" duration="34104000" />
|
||||||
<workItem from="1603714609431" duration="5637000" />
|
<workItem from="1603714609431" duration="5637000" />
|
||||||
<workItem from="1603720261881" duration="8249000" />
|
<workItem from="1603720261881" duration="6236000" />
|
||||||
<workItem from="1605688147310" duration="310000" />
|
|
||||||
<workItem from="1610959328356" duration="9022000" />
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="mp3 support! more helpful interface! better code! yahoo!!">
|
<task id="LOCAL-00001" summary="mp3 support! more helpful interface! better code! yahoo!!">
|
||||||
<created>1602927759343</created>
|
<created>1602927759343</created>
|
||||||
|
@ -224,14 +183,7 @@
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1603814270092</updated>
|
<updated>1603814270092</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00012" summary="check against tag format type instead of file extension sorta like, "if tag_format == 'id3'" rather than "if song_format == ['mp3', 'wav', 'aiff']"">
|
<option name="localTasksCounter" value="12" />
|
||||||
<created>1603888340561</created>
|
|
||||||
<option name="number" value="00012" />
|
|
||||||
<option name="presentableId" value="LOCAL-00012" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1603888340561</updated>
|
|
||||||
</task>
|
|
||||||
<option name="localTasksCounter" value="13" />
|
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
@ -250,7 +202,6 @@
|
||||||
<option name="oldMeFiltersMigrated" value="true" />
|
<option name="oldMeFiltersMigrated" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
|
||||||
<MESSAGE value="mp3 support! more helpful interface! better code! yahoo!!" />
|
<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="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="mypy integration" />
|
||||||
|
@ -262,12 +213,116 @@
|
||||||
<MESSAGE value="added project files, aiff support" />
|
<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="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" />
|
<MESSAGE value="remove unneeded file extension" />
|
||||||
<MESSAGE value="check against tag format type instead of file extension sorta like, "if tag_format == 'id3'" rather than "if song_format == ['mp3', 'wav', 'aiff']"" />
|
<option name="LAST_COMMIT_MESSAGE" value="remove unneeded file extension" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="check against tag format type instead of file extension sorta like, "if tag_format == 'id3'" rather than "if song_format == ['mp3', 'wav', 'aiff']"" />
|
</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>
|
||||||
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
<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.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$mypy.coverage" NAME="mypy Coverage Results" MODIFIED="1610962969226" 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$" />
|
||||||
<SUITE FILE_PATH="coverage/bcao$bcao__ceres_.coverage" NAME="bcao (ceres) Coverage Results" MODIFIED="1610962396086" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -12,11 +12,11 @@
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
<option name="SCRIPT_NAME" value="bcao" />
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/bcao.py" />
|
||||||
<option name="PARAMETERS" value=""A Cerulean State - As if I remembered something.zip" -d "/tmp/out/"" />
|
<option name="PARAMETERS" value=""A Cerulean State - As if I remembered something.zip" -d "/tmp/out/"" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
<option name="EMULATE_TERMINAL" value="false" />
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
<option name="MODULE_MODE" value="true" />
|
<option name="MODULE_MODE" value="false" />
|
||||||
<option name="REDIRECT_INPUT" value="false" />
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
<option name="INPUT_FILE" value="" />
|
<option name="INPUT_FILE" value="" />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/bcao/__main__.py" />
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/bcao.py" />
|
||||||
<option name="PARAMETERS" value=""Braxton Burks - Time & Space.zip" -d "$USER_HOME$/Documents"" />
|
<option name="PARAMETERS" value=""Braxton Burks - Time & Space.zip" -d "$USER_HOME$/Documents"" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
<option name="EMULATE_TERMINAL" value="false" />
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
|
|
@ -6,14 +6,14 @@
|
||||||
<envs>
|
<envs>
|
||||||
<env name="PYTHONUNBUFFERED" value="1" />
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="SDK_HOME" value="/usr/bin/python3.9" />
|
<option name="SDK_HOME" value="" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
<option name="IS_MODULE_SDK" value="false" />
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
<option name="SCRIPT_NAME" value="$USER_HOME$/.local/bin/mypy" />
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/venv/bin/mypy" />
|
||||||
<option name="PARAMETERS" value="bcao" />
|
<option name="PARAMETERS" value="bcao.py" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
<option name="EMULATE_TERMINAL" value="false" />
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
<option name="MODULE_MODE" value="false" />
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
|
39
README.md
39
README.md
|
@ -1,39 +0,0 @@
|
||||||
bcao
|
|
||||||
====
|
|
||||||
|
|
||||||
bandcamp album organiser - a python script to organise, rename, and apply cover art to zip files downloaded from bandcamp.
|
|
||||||
|
|
||||||
## usage
|
|
||||||
```
|
|
||||||
./bcao.py [zip file] [options]
|
|
||||||
```
|
|
||||||
see the help menu (`--help`) for more.
|
|
||||||
|
|
||||||
## installing
|
|
||||||
### the very easy way
|
|
||||||
download [bcao.pex](https://git.bune.city/lynnesbian/bcao/raw/branch/master/bcao.pex) and run it:
|
|
||||||
```
|
|
||||||
python bcao.pex [album name.zip]
|
|
||||||
```
|
|
||||||
|
|
||||||
### the pretty easy way
|
|
||||||
requires [poetry](https://python-poetry.org/).
|
|
||||||
```
|
|
||||||
git clone https://git.bune.city/lynnesbian/bcao
|
|
||||||
cd bcao
|
|
||||||
poetry install
|
|
||||||
poetry run bcao
|
|
||||||
```
|
|
||||||
|
|
||||||
### the other way
|
|
||||||
```
|
|
||||||
git clone https://git.bune.city/lynnesbian/bcao
|
|
||||||
cd bcao
|
|
||||||
virtualenv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
python -m bcao
|
|
||||||
```
|
|
||||||
|
|
||||||
## building it yourself
|
|
||||||
see `build.sh`
|
|
BIN
bcao.pex
BIN
bcao.pex
Binary file not shown.
|
@ -5,9 +5,6 @@
|
||||||
# input: a .zip from bandcamp
|
# input: a .zip from bandcamp
|
||||||
# output: it organises it, adds cover art, puts it in the right place...
|
# output: it organises it, adds cover art, puts it in the right place...
|
||||||
|
|
||||||
from bcao import *
|
|
||||||
from bcao.song_info import SongInfo
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
@ -16,21 +13,182 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
from os import path
|
from os import path
|
||||||
|
from base64 import b64encode
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from typing import Optional, List, Dict
|
|
||||||
from pathlib import Path
|
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
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
from mutagen.flac import Picture
|
from mutagen.flac import Picture, FLAC
|
||||||
from mutagen.mp4 import MP4Cover
|
from mutagen.oggvorbis import OggVorbis
|
||||||
|
from mutagen.mp3 import MP3
|
||||||
|
from mutagen.mp4 import MP4, MP4Cover
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
from mutagen.id3 import APIC, PictureType
|
from mutagen.id3 import APIC, PictureType, Frame, TRCK, TPE1, TIT2, TALB, TPE2
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
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
|
args: argparse.Namespace
|
||||||
tmp_dir: tempfile.TemporaryDirectory # type: ignore
|
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:
|
def log(message: str, importance: int = 0) -> None:
|
||||||
if not args.quiet or importance > 0:
|
if not args.quiet or importance > 0:
|
||||||
print(message)
|
print(message)
|
||||||
|
@ -44,9 +202,10 @@ def die(message: str, code: int = 1) -> None:
|
||||||
|
|
||||||
def sanitise(in_str: str) -> str:
|
def sanitise(in_str: str) -> str:
|
||||||
if args.sanitise:
|
if args.sanitise:
|
||||||
return re.sub(sanitisation_regex, "_", in_str)
|
return re.sub(r"[?\\/:|*\"<>]", "_", in_str)
|
||||||
return in_str
|
return in_str
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
global args, tmp_dir
|
global args, tmp_dir
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
import re
|
|
||||||
|
|
||||||
import mutagen
|
|
||||||
from mutagen.flac import FLAC
|
|
||||||
from mutagen.mp3 import MP3
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
from mutagen.id3 import ID3Tags
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
from mutagen.mp4 import Tags
|
|
||||||
from mutagen.oggvorbis import OggVorbis
|
|
||||||
|
|
||||||
from typing import Dict, List, Union
|
|
||||||
|
|
||||||
format_lookup: Dict[str, str] = {
|
|
||||||
"mp3": "id3",
|
|
||||||
"m4a": "m4a",
|
|
||||||
"ogg": "vorbis",
|
|
||||||
"flac": "vorbis",
|
|
||||||
"wav": "id3",
|
|
||||||
"aiff": "id3"
|
|
||||||
}
|
|
||||||
fully_supported: List[str] = ["ogg", "flac", "mp3", "m4a", "wav", "aiff"]
|
|
||||||
MutagenFile = Union[MP3, FLAC, OggVorbis, mutagen.FileType]
|
|
||||||
MutagenTags = Union[ID3Tags, Tags, mutagen.oggvorbis.OggVCommentDict]
|
|
||||||
sanitisation_regex = re.compile(r"[?\\/:|*\"<>]")
|
|
||||||
|
|
|
@ -1,161 +0,0 @@
|
||||||
from bcao import *
|
|
||||||
|
|
||||||
import re
|
|
||||||
from os import path
|
|
||||||
from typing import Union, List, Dict
|
|
||||||
from pathlib import Path
|
|
||||||
from base64 import b64encode
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
from mutagen.flac import Picture
|
|
||||||
from mutagen.mp4 import MP4Cover
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
from mutagen.id3 import APIC, PictureType, Frame, TRCK, TPE1, TIT2, TALB, TPE2
|
|
||||||
|
|
||||||
class FallbackError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
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:
|
|
||||||
raise FallbackError("Couldn't determine fallback tags!")
|
|
||||||
|
|
||||||
# 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 = [re.sub(sanitisation_regex, "_", 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]
|
|
6
build.sh
6
build.sh
|
@ -1,6 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# poetry build
|
|
||||||
poetry install
|
|
||||||
poetry export --without-hashes -o requirements.txt
|
|
||||||
pex . -r requirements.txt -e bcao -o bcao.pex
|
|
60
poetry.lock
generated
60
poetry.lock
generated
|
@ -1,60 +0,0 @@
|
||||||
[[package]]
|
|
||||||
name = "mutagen"
|
|
||||||
version = "1.45.1"
|
|
||||||
description = "read and write audio tags for many formats"
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5, <4"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pillow"
|
|
||||||
version = "8.1.0"
|
|
||||||
description = "Python Imaging Library (Fork)"
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
lock-version = "1.1"
|
|
||||||
python-versions = "^3.9"
|
|
||||||
content-hash = "cfad5bb42d73e5820410175ccbb716fa1ca4fa093a3fd3db1aebde1e68ce282e"
|
|
||||||
|
|
||||||
[metadata.files]
|
|
||||||
mutagen = [
|
|
||||||
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
|
||||||
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
|
||||||
]
|
|
||||||
pillow = [
|
|
||||||
{file = "Pillow-8.1.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a"},
|
|
||||||
{file = "Pillow-8.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2"},
|
|
||||||
{file = "Pillow-8.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174"},
|
|
||||||
{file = "Pillow-8.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded"},
|
|
||||||
{file = "Pillow-8.1.0-cp36-cp36m-win32.whl", hash = "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d"},
|
|
||||||
{file = "Pillow-8.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d"},
|
|
||||||
{file = "Pillow-8.1.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234"},
|
|
||||||
{file = "Pillow-8.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8"},
|
|
||||||
{file = "Pillow-8.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17"},
|
|
||||||
{file = "Pillow-8.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7"},
|
|
||||||
{file = "Pillow-8.1.0-cp37-cp37m-win32.whl", hash = "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e"},
|
|
||||||
{file = "Pillow-8.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b"},
|
|
||||||
{file = "Pillow-8.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0"},
|
|
||||||
{file = "Pillow-8.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a"},
|
|
||||||
{file = "Pillow-8.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d"},
|
|
||||||
{file = "Pillow-8.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae"},
|
|
||||||
{file = "Pillow-8.1.0-cp38-cp38-win32.whl", hash = "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59"},
|
|
||||||
{file = "Pillow-8.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c"},
|
|
||||||
{file = "Pillow-8.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6"},
|
|
||||||
{file = "Pillow-8.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378"},
|
|
||||||
{file = "Pillow-8.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7"},
|
|
||||||
{file = "Pillow-8.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0"},
|
|
||||||
{file = "Pillow-8.1.0-cp39-cp39-win32.whl", hash = "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b"},
|
|
||||||
{file = "Pillow-8.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865"},
|
|
||||||
{file = "Pillow-8.1.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9"},
|
|
||||||
{file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913"},
|
|
||||||
{file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206"},
|
|
||||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9"},
|
|
||||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032"},
|
|
||||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820"},
|
|
||||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5"},
|
|
||||||
{file = "Pillow-8.1.0.tar.gz", hash = "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba"},
|
|
||||||
]
|
|
|
@ -1,20 +0,0 @@
|
||||||
[tool.poetry]
|
|
||||||
name = "bcao"
|
|
||||||
version = "1.0.0"
|
|
||||||
description = "Bandcamp Album Organiser"
|
|
||||||
authors = ["Lynne <lynne@bune.city>"]
|
|
||||||
license = "GPL3"
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.9"
|
|
||||||
mutagen = "^1.45.1"
|
|
||||||
Pillow = "^8.1.0"
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
|
||||||
bcao = "bcao.__main__:main"
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core>=1.0.0"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
|
@ -1,2 +1,2 @@
|
||||||
mutagen==1.45.1; python_version >= "3.5" and python_version < "4"
|
mutagen~=1.45
|
||||||
pillow==8.1.0; python_version >= "3.6"
|
Pillow~=8.0
|
||||||
|
|
Loading…
Reference in a new issue