Compare commits

..

No commits in common. "e721a85fd2087c281eeb169a88b994872e8358c2" and "d27e1481c33f8d1ac375e547ebe167256b3d3506" have entirely different histories.

16 changed files with 295 additions and 401 deletions

11
.gitignore vendored
View file

@ -1,14 +1,7 @@
*
!*/
__pycache__/**
!bcao/*.py
!bcao.py
!requirements.txt
!poetry.lock
!pyproject.toml
!bcao.pex
!.gitignore
!mypy.ini
!.run/
!.idea/
!README.md
!build.sh
*

View file

@ -7,7 +7,7 @@
<excludeFolder url="file://$MODULE_DIR$/test" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</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" />
</component>
</module>

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>

View file

@ -20,27 +20,14 @@
</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$/.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$/bcao/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/bcao/__init__.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" />
<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="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Python Script" />
</list>
</option>
</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" />
@ -56,51 +43,25 @@
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
<option name="RESET_MODE" value="HARD" />
</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="ProjectId" id="1iwv1rbtMpCLK7D695td98N37pr" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<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="WebServerToolWindowFactoryState" value="false" />
<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.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="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" />
</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>
<property name="settings.editor.selected.configurable" value="preferences.pluginManager" />
</component>
<component name="RunManager" selected="Python.bcao (ceres)">
<configuration default="true" type="PythonConfigurationType" factoryName="Python">
@ -126,8 +87,8 @@
<method v="2" />
</configuration>
<list>
<item itemvalue="Python.bcao (ceres)" />
<item itemvalue="Python.bcao (io)" />
<item itemvalue="Python.bcao (ceres)" />
<item itemvalue="Python.mypy" />
</list>
</component>
@ -143,9 +104,7 @@
<workItem from="1602850978698" duration="7902000" />
<workItem from="1602908398925" duration="34104000" />
<workItem from="1603714609431" duration="5637000" />
<workItem from="1603720261881" duration="8249000" />
<workItem from="1605688147310" duration="310000" />
<workItem from="1610959328356" duration="9022000" />
<workItem from="1603720261881" duration="6236000" />
</task>
<task id="LOCAL-00001" summary="mp3 support! more helpful interface! better code! yahoo!!">
<created>1602927759343</created>
@ -224,14 +183,7 @@
<option name="project" value="LOCAL" />
<updated>1603814270092</updated>
</task>
<task id="LOCAL-00012" summary="check against tag format type instead of file extension&#10;&#10;sorta like, &quot;if tag_format == 'id3'&quot; rather than &quot;if song_format == ['mp3', 'wav', 'aiff']&quot;">
<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" />
<option name="localTasksCounter" value="12" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@ -250,7 +202,6 @@
<option name="oldMeFiltersMigrated" value="true" />
</component>
<component name="VcsManagerConfiguration">
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
<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" />
@ -262,12 +213,116 @@
<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" />
<MESSAGE value="check against tag format type instead of file extension&#10;&#10;sorta like, &quot;if tag_format == 'id3'&quot; rather than &quot;if song_format == ['mp3', 'wav', 'aiff']&quot;" />
<option name="LAST_COMMIT_MESSAGE" value="check against tag format type instead of file extension&#10;&#10;sorta like, &quot;if tag_format == 'id3'&quot; rather than &quot;if song_format == ['mp3', 'wav', 'aiff']&quot;" />
<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$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="1610962396086" 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>

View file

@ -12,11 +12,11 @@
<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="bcao" />
<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="true" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />

View file

@ -12,7 +12,7 @@
<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/__main__.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" />

View file

@ -6,14 +6,14 @@
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="/usr/bin/python3.9" />
<option name="SDK_HOME" value="" />
<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_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$USER_HOME$/.local/bin/mypy" />
<option name="PARAMETERS" value="bcao" />
<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" />

View file

@ -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

Binary file not shown.

View file

@ -5,9 +5,6 @@
# input: a .zip from bandcamp
# 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 io
import os
@ -16,21 +13,182 @@ import sys
import tempfile
import shutil
from os import path
from base64 import b64encode
from zipfile import ZipFile
from typing import Optional, List, Dict
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
from mutagen.flac import Picture
from mutagen.mp4 import MP4Cover
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
from mutagen.id3 import APIC, PictureType, Frame, TRCK, TPE1, TIT2, TALB, TPE2
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
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)
@ -44,9 +202,10 @@ def die(message: str, code: int = 1) -> None:
def sanitise(in_str: str) -> str:
if args.sanitise:
return re.sub(sanitisation_regex, "_", in_str)
return re.sub(r"[?\\/:|*\"<>]", "_", in_str)
return in_str
def main() -> None:
global args, tmp_dir

View file

@ -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"[?\\/:|*\"<>]")

View file

@ -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]

View file

@ -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
View file

@ -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"},
]

View file

@ -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"

View file

@ -1,2 +1,2 @@
mutagen==1.45.1; python_version >= "3.5" and python_version < "4"
pillow==8.1.0; python_version >= "3.6"
mutagen~=1.45
Pillow~=8.0