Compare commits
77 Commits
0ae5a99346
...
fe4b1f8b5e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4b1f8b5e | ||
|
|
1abee60827 | ||
|
|
6ca37bc45a | ||
|
|
558a283e73 | ||
|
|
fe660524a0 | ||
|
|
805053b795 | ||
|
|
c5f33c437f | ||
|
|
0a3700e208 | ||
|
|
976eb91e30 | ||
|
|
ebfdf98612 | ||
|
|
0fb1536055 | ||
|
|
ca385dcf54 | ||
|
|
0d865f05ac | ||
|
|
75b814e26c | ||
|
|
47f53428f6 | ||
|
|
e2c5bba4ae | ||
|
|
7f046ae86b | ||
|
|
a27dd7189a | ||
|
|
34fa0c92b2 | ||
|
|
87f9e1e81b | ||
|
|
a31718d2b9 | ||
|
|
cf4e42358e | ||
|
|
4ce6c2e9b9 | ||
|
|
fbca77debe | ||
|
|
3ebb3e1acf | ||
|
|
f0b9ab4256 | ||
|
|
2b02c1b5b4 | ||
|
|
a882d409cb | ||
|
|
2186b3eb09 | ||
|
|
06efaf2ba2 | ||
|
|
9c0371d41c | ||
|
|
e7004688d0 | ||
|
|
8c69f108cb | ||
|
|
f22f209bee | ||
|
|
1c56505ab0 | ||
|
|
ca1b11b545 | ||
|
|
9397adee03 | ||
|
|
4a83e9af86 | ||
|
|
f22f2780a3 | ||
|
|
f1aba41921 | ||
|
|
a2fb6baba8 | ||
|
|
08eea631d6 | ||
|
|
d62a044522 | ||
|
|
e8211414f9 | ||
|
|
26edd5a2d0 | ||
|
|
bc6a4c11cf | ||
|
|
a91309477b | ||
|
|
3a7b09f025 | ||
|
|
7f2dd68bce | ||
|
|
281a1d40bf | ||
|
|
cf58932fca | ||
|
|
b92a0927f8 | ||
|
|
ab9955b88a | ||
|
|
b00f70ff4b | ||
|
|
9fb05079dc | ||
|
|
1c86728170 | ||
|
|
557b89ba09 | ||
|
|
7cd2d610b1 | ||
|
|
ac27486317 | ||
|
|
907861ea48 | ||
|
|
04c3c2efbc | ||
|
|
fa2e1234e9 | ||
|
|
fec45925c6 | ||
|
|
fcebe2f220 | ||
|
|
00f85a9a96 | ||
|
|
f3bf829ef3 | ||
|
|
1a0cac22f6 | ||
|
|
9aa6941fca | ||
|
|
a164f4c962 | ||
|
|
db86d04b9a | ||
|
|
3cab7a8376 | ||
|
|
2015dcce1f | ||
|
|
03735c2456 | ||
|
|
b283a3db07 | ||
|
|
cb50fc253b | ||
|
|
9d78e5fe57 | ||
|
|
bb648488d6 |
10
.envrc
10
.envrc
@ -1 +1,11 @@
|
|||||||
layout poetry
|
layout poetry
|
||||||
|
MYSQL_USER="musicmuster"
|
||||||
|
MYSQL_PASSWORD="musicmuster"
|
||||||
|
branch=$(git branch --show-current)
|
||||||
|
if on_git_branch master; then
|
||||||
|
MYSQL_DATABASE="musicmuster_prod"
|
||||||
|
elif on_git_branch v2; then
|
||||||
|
MYSQL_DATABASE="musicmuster_v2"
|
||||||
|
else MYSQL_DATABASE="musicmuster_dev"
|
||||||
|
fi
|
||||||
|
export MYSQL_CONNECT="mysql+mysqldb://${MYSQL_USER}:${MYSQL_PASSWORD}@localhost/${MYSQL_DATABASE}"
|
||||||
|
|||||||
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
3
.idea/dictionaries/kae.xml
Normal file
3
.idea/dictionaries/kae.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="kae" />
|
||||||
|
</component>
|
||||||
@ -1,4 +1,7 @@
|
|||||||
<?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="Poetry (musicmuster) (2)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (musicmuster)" project-jdk-type="Python SDK" />
|
||||||
|
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@ -1,8 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="PYTHON_MODULE" version="4">
|
<module type="PYTHON_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$">
|
||||||
<orderEntry type="inheritedJdk" />
|
<sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Poetry (musicmuster)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PyDocumentationSettings">
|
<component name="PyDocumentationSettings">
|
||||||
|
|||||||
@ -39,7 +39,10 @@ prepend_sys_path = .
|
|||||||
# are written from script.py.mako
|
# are written from script.py.mako
|
||||||
# output_encoding = utf-8
|
# output_encoding = utf-8
|
||||||
|
|
||||||
sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_dev
|
sqlalchemy.url = SET
|
||||||
|
# sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod
|
||||||
|
# sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_dev
|
||||||
|
# sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_v2
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
# post_write_hooks defines scripts or Python functions that are run
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
|||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
@ -3,29 +3,43 @@ import os
|
|||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
|
AUDACITY_COMMAND = "/usr/bin/audacity"
|
||||||
AUDIO_SEGMENT_CHUNK_SIZE = 10
|
AUDIO_SEGMENT_CHUNK_SIZE = 10
|
||||||
|
CHECK_AUDACITY_AT_STARTUP = True
|
||||||
COLOUR_CURRENT_HEADER = "#d4edda"
|
COLOUR_CURRENT_HEADER = "#d4edda"
|
||||||
COLOUR_CURRENT_PLAYLIST = "#28a745"
|
COLOUR_CURRENT_PLAYLIST = "#7eca8f"
|
||||||
COLOUR_CURRENT_TAB = "#248f24"
|
COLOUR_CURRENT_TAB = "#248f24"
|
||||||
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
|
||||||
COLOUR_ENDING_TIMER = "#dc3545"
|
COLOUR_ENDING_TIMER = "#dc3545"
|
||||||
COLOUR_EVEN_PLAYLIST = "#d9d9d9"
|
COLOUR_EVEN_PLAYLIST = "#d9d9d9"
|
||||||
COLOUR_LONG_START = "#dc3545"
|
COLOUR_LONG_START = "#dc3545"
|
||||||
COLOUR_NORMAL_TAB = "#000000"
|
|
||||||
COLOUR_NEXT_HEADER = "#fff3cd"
|
COLOUR_NEXT_HEADER = "#fff3cd"
|
||||||
COLOUR_NEXT_PLAYLIST = "#ffc107"
|
COLOUR_NEXT_PLAYLIST = "#ffc107"
|
||||||
COLOUR_NEXT_TAB = "#b38600"
|
COLOUR_NEXT_TAB = "#b38600"
|
||||||
|
COLOUR_NORMAL_TAB = "#000000"
|
||||||
COLOUR_NOTES_PLAYLIST = "#b8daff"
|
COLOUR_NOTES_PLAYLIST = "#b8daff"
|
||||||
|
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
||||||
COLOUR_PREVIOUS_HEADER = "#f8d7da"
|
COLOUR_PREVIOUS_HEADER = "#f8d7da"
|
||||||
COLOUR_UNREADABLE = "#dc3545"
|
COLOUR_UNREADABLE = "#dc3545"
|
||||||
COLOUR_WARNING_TIMER = "#ffc107"
|
COLOUR_WARNING_TIMER = "#ffc107"
|
||||||
|
COLUMN_NAME_ARTIST = "Artist"
|
||||||
|
COLUMN_NAME_AUTOPLAY = "A"
|
||||||
|
COLUMN_NAME_END_TIME = "End"
|
||||||
|
COLUMN_NAME_LAST_PLAYED = "Last played"
|
||||||
|
COLUMN_NAME_LEADING_SILENCE = "Gap"
|
||||||
|
COLUMN_NAME_LENGTH = "Length"
|
||||||
|
COLUMN_NAME_START_TIME = "Start"
|
||||||
|
COLUMN_NAME_TITLE = "Title"
|
||||||
DBFS_FADE = -12
|
DBFS_FADE = -12
|
||||||
DBFS_SILENCE = -50
|
DBFS_SILENCE = -50
|
||||||
|
DEFAULT_COLUMN_WIDTH = 200
|
||||||
|
DEFAULT_IMPORT_DIRECTORY = "/home/kae/Nextcloud/tmp"
|
||||||
|
DEFAULT_OUTPUT_DIRECTORY = "/home/kae/music/Singles"
|
||||||
DISPLAY_SQL = False
|
DISPLAY_SQL = False
|
||||||
ERRORS_TO = ['kae@midnighthax.com']
|
ERRORS_TO = ['kae@midnighthax.com']
|
||||||
FADE_STEPS = 20
|
FADE_STEPS = 20
|
||||||
FADE_TIME = 3000
|
FADE_TIME = 3000
|
||||||
INFO_TAB_TITLE_LENGTH = 15
|
INFO_TAB_TITLE_LENGTH = 15
|
||||||
|
INFO_TAB_URL = "https://www.wikipedia.org/w/index.php?search=%s"
|
||||||
LOG_LEVEL_STDERR = logging.INFO
|
LOG_LEVEL_STDERR = logging.INFO
|
||||||
LOG_LEVEL_SYSLOG = logging.DEBUG
|
LOG_LEVEL_SYSLOG = logging.DEBUG
|
||||||
LOG_NAME = "musicmuster"
|
LOG_NAME = "musicmuster"
|
||||||
@ -36,19 +50,17 @@ class Config(object):
|
|||||||
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
|
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
|
||||||
MAX_INFO_TABS = 3
|
MAX_INFO_TABS = 3
|
||||||
MILLISECOND_SIGFIGS = 0
|
MILLISECOND_SIGFIGS = 0
|
||||||
MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_dev" # noqa E501
|
MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_v2" # noqa E501
|
||||||
NORMALISE_ON_IMPORT = True
|
NORMALISE_ON_IMPORT = True
|
||||||
NOTE_COLOURS = {
|
NOTE_TIME_FORMAT = "%H:%M:%S"
|
||||||
'track': "#ffff00",
|
|
||||||
'request': "#7cf000",
|
|
||||||
'wrap': "#fffacd",
|
|
||||||
'this month then': "#c256c2",
|
|
||||||
'story': "#dda0dd",
|
|
||||||
}
|
|
||||||
ROOT = os.environ.get('ROOT') or "/home/kae/music"
|
ROOT = os.environ.get('ROOT') or "/home/kae/music"
|
||||||
|
SCROLL_TOP_MARGIN = 3
|
||||||
TESTMODE = True
|
TESTMODE = True
|
||||||
|
TOD_TIME_FORMAT = "%H:%M:%S"
|
||||||
TIMER_MS = 500
|
TIMER_MS = 500
|
||||||
|
TRACK_TIME_FORMAT = "%H:%M:%S"
|
||||||
VOLUME_VLC_DEFAULT = 75
|
VOLUME_VLC_DEFAULT = 75
|
||||||
|
WEB_ZOOM_FACTOR = 1.4
|
||||||
|
|
||||||
|
|
||||||
config = Config
|
config = Config
|
||||||
|
|||||||
72
app/dbconfig.py
Normal file
72
app/dbconfig.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from log import DEBUG
|
||||||
|
from sqlalchemy.orm import (sessionmaker, scoped_session)
|
||||||
|
|
||||||
|
|
||||||
|
class Counter:
|
||||||
|
def __init__(self):
|
||||||
|
self.count = 0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return(f"<Counter({self.count=})>")
|
||||||
|
|
||||||
|
def inc(self):
|
||||||
|
self.count += 1
|
||||||
|
return self.count
|
||||||
|
|
||||||
|
def dec(self):
|
||||||
|
self.count -= 1
|
||||||
|
return self.count
|
||||||
|
|
||||||
|
|
||||||
|
MM_ENV = os.environ.get('MM_ENV', 'PRODUCTION')
|
||||||
|
testing = False
|
||||||
|
|
||||||
|
if MM_ENV == 'PRODUCTION':
|
||||||
|
dbname = os.environ.get('MM_PRODUCTION_DBNAME', 'musicmuster_prod')
|
||||||
|
dbuser = os.environ.get('MM_PRODUCTION_DBUSER', 'musicmuster')
|
||||||
|
dbpw = os.environ.get('MM_PRODUCTION_DBPW', 'musicmuster')
|
||||||
|
dbhost = os.environ.get('MM_PRODUCTION_DBHOST', 'localhost')
|
||||||
|
elif MM_ENV == 'TESTING':
|
||||||
|
dbname = os.environ.get('MM_TESTING_DBNAME', 'musicmuster_testing')
|
||||||
|
dbuser = os.environ.get('MM_TESTING_DBUSER', 'musicmuster_testing')
|
||||||
|
dbpw = os.environ.get('MM_TESTING_DBPW', 'musicmuster_testing')
|
||||||
|
dbhost = os.environ.get('MM_TESTING_DBHOST', 'localhost')
|
||||||
|
testing = True
|
||||||
|
elif MM_ENV == 'DEVELOPMENT':
|
||||||
|
dbname = os.environ.get('MM_DEVELOPMENT_DBNAME', 'musicmuster_dev')
|
||||||
|
dbuser = os.environ.get('MM_DEVELOPMENT_DBUSER', 'musicmuster')
|
||||||
|
dbpw = os.environ.get('MM_DEVELOPMENT_DBPW', 'musicmuster')
|
||||||
|
dbhost = os.environ.get('MM_DEVELOPMENT_DBHOST', 'localhost')
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown MusicMuster environment: {MM_ENV=}")
|
||||||
|
|
||||||
|
MYSQL_CONNECT = f"mysql+mysqldb://{dbuser}:{dbpw}@{dbhost}/{dbname}"
|
||||||
|
|
||||||
|
engine = sqlalchemy.create_engine(
|
||||||
|
MYSQL_CONNECT,
|
||||||
|
encoding='utf-8',
|
||||||
|
echo=Config.DISPLAY_SQL,
|
||||||
|
pool_pre_ping=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Session = scoped_session(sessionmaker(bind=engine))
|
||||||
|
@contextmanager
|
||||||
|
def Session():
|
||||||
|
frame = inspect.stack()[2]
|
||||||
|
file = frame.filename
|
||||||
|
function = frame.function
|
||||||
|
lineno = frame.lineno
|
||||||
|
Session = scoped_session(sessionmaker(bind=engine))
|
||||||
|
DEBUG(f"Session acquired, {file=}, {function=}, {lineno=}, {Session=}",
|
||||||
|
True)
|
||||||
|
yield Session
|
||||||
|
DEBUG(" Session released", True)
|
||||||
|
Session.commit()
|
||||||
|
Session.close()
|
||||||
199
app/helpers.py
199
app/helpers.py
@ -1,11 +1,78 @@
|
|||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
|
from config import Config
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pydub import AudioSegment
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
from tinytag import TinyTag
|
||||||
|
from typing import Dict, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
def get_relative_date(past_date, reference_date=None):
|
def ask_yes_no(title: str, question: str) -> bool:
|
||||||
|
"""Ask question; return True for yes, False for no"""
|
||||||
|
|
||||||
|
button_reply: bool = QMessageBox.question(None, title, question)
|
||||||
|
|
||||||
|
return button_reply == QMessageBox.Yes
|
||||||
|
|
||||||
|
|
||||||
|
def fade_point(
|
||||||
|
audio_segment: AudioSegment, fade_threshold: int = 0,
|
||||||
|
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
||||||
|
"""
|
||||||
|
Returns the millisecond/index of the point where the volume drops below
|
||||||
|
the maximum and doesn't get louder again.
|
||||||
|
audio_segment - the sdlg_search_database_uiegment to find silence in
|
||||||
|
fade_threshold - the upper bound for how quiet is silent in dFBS
|
||||||
|
chunk_size - chunk size for interating over the segment in ms
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert chunk_size > 0 # to avoid infinite loop
|
||||||
|
|
||||||
|
segment_length: int = audio_segment.duration_seconds * 1000 # ms
|
||||||
|
trim_ms: int = segment_length - chunk_size
|
||||||
|
max_vol: int = audio_segment.dBFS
|
||||||
|
if fade_threshold == 0:
|
||||||
|
fade_threshold = max_vol
|
||||||
|
|
||||||
|
while (
|
||||||
|
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
|
||||||
|
and trim_ms > 0): # noqa W503
|
||||||
|
trim_ms -= chunk_size
|
||||||
|
|
||||||
|
# if there is no trailing silence, return lenght of track (it's less
|
||||||
|
# the chunk_size, but for chunk_size = 10ms, this may be ignored)
|
||||||
|
return int(trim_ms)
|
||||||
|
|
||||||
|
|
||||||
|
def get_audio_segment(path: str) -> Optional[AudioSegment]:
|
||||||
|
try:
|
||||||
|
if path.endswith('.mp3'):
|
||||||
|
return AudioSegment.from_mp3(path)
|
||||||
|
elif path.endswith('.flac'):
|
||||||
|
return AudioSegment.from_file(path, "flac")
|
||||||
|
except AttributeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_tags(path: str) -> Dict[str, Union[str, int]]:
|
||||||
|
"""
|
||||||
|
Return a dictionary of title, artist, duration-in-milliseconds and path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tag: TinyTag = TinyTag.get(path)
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
title=tag.title,
|
||||||
|
artist=tag.artist,
|
||||||
|
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
|
||||||
|
path=path
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_relative_date(past_date: datetime, reference_date: datetime = None) \
|
||||||
|
-> str:
|
||||||
"""
|
"""
|
||||||
Return how long before reference_date past_date is as string.
|
Return how long before reference_date past_date is as string.
|
||||||
|
|
||||||
@ -25,6 +92,11 @@ def get_relative_date(past_date, reference_date=None):
|
|||||||
if past_date > reference_date:
|
if past_date > reference_date:
|
||||||
return "get_relative_date() past_date is after relative_date"
|
return "get_relative_date() past_date is after relative_date"
|
||||||
|
|
||||||
|
days: int
|
||||||
|
days_str: str
|
||||||
|
weeks: int
|
||||||
|
weeks_str: str
|
||||||
|
|
||||||
weeks, days = divmod((reference_date.date() - past_date.date()).days, 7)
|
weeks, days = divmod((reference_date.date() - past_date.date()).days, 7)
|
||||||
if weeks == days == 0:
|
if weeks == days == 0:
|
||||||
# Played today, so return time instead
|
# Played today, so return time instead
|
||||||
@ -40,55 +112,37 @@ def get_relative_date(past_date, reference_date=None):
|
|||||||
return f"{weeks} {weeks_str}, {days} {days_str} ago"
|
return f"{weeks} {weeks_str}, {days} {days_str} ago"
|
||||||
|
|
||||||
|
|
||||||
def open_in_audacity(path):
|
def leading_silence(
|
||||||
|
audio_segment: AudioSegment,
|
||||||
|
silence_threshold: int = Config.DBFS_SILENCE,
|
||||||
|
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
||||||
"""
|
"""
|
||||||
Open passed file in Audacity
|
Returns the millisecond/index that the leading silence ends.
|
||||||
|
audio_segment - the segment to find silence in
|
||||||
|
silence_threshold - the upper bound for how quiet is silent in dFBS
|
||||||
|
chunk_size - chunk size for interating over the segment in ms
|
||||||
|
|
||||||
Return True if apparently opened successfully, else False
|
https://github.com/jiaaro/pydub/blob/master/pydub/silence.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Return if audacity not running
|
trim_ms: int = 0 # ms
|
||||||
if "audacity" not in [i.name() for i in psutil.process_iter()]:
|
assert chunk_size > 0 # to avoid infinite loop
|
||||||
return False
|
while (
|
||||||
|
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504
|
||||||
|
silence_threshold and trim_ms < len(audio_segment)):
|
||||||
|
trim_ms += chunk_size
|
||||||
|
|
||||||
to_pipe = '/tmp/audacity_script_pipe.to.' + str(os.getuid())
|
# if there is no end it should return the length of the segment
|
||||||
from_pipe = '/tmp/audacity_script_pipe.from.' + str(os.getuid())
|
return min(trim_ms, len(audio_segment))
|
||||||
EOL = '\n'
|
|
||||||
|
|
||||||
def send_command(command):
|
|
||||||
"""Send a single command."""
|
|
||||||
to_audacity.write(command + EOL)
|
|
||||||
to_audacity.flush()
|
|
||||||
|
|
||||||
def get_response():
|
|
||||||
"""Return the command response."""
|
|
||||||
result = ''
|
|
||||||
line = ''
|
|
||||||
while True:
|
|
||||||
result += line
|
|
||||||
line = from_audacity.readline()
|
|
||||||
if line == '\n' and len(result) > 0:
|
|
||||||
break
|
|
||||||
return result
|
|
||||||
|
|
||||||
def do_command(command):
|
|
||||||
"""Send one command, and return the response."""
|
|
||||||
send_command(command)
|
|
||||||
response = get_response()
|
|
||||||
return response
|
|
||||||
|
|
||||||
with open(to_pipe, 'w') as to_audacity, open(
|
|
||||||
from_pipe, 'rt') as from_audacity:
|
|
||||||
do_command(f'Import2: Filename="{path}"')
|
|
||||||
|
|
||||||
|
|
||||||
def show_warning(title, msg):
|
def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str:
|
||||||
"Display a warning to user"
|
"""Convert milliseconds to mm:ss"""
|
||||||
|
|
||||||
QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)
|
minutes: int
|
||||||
|
remainder: int
|
||||||
|
seconds: float
|
||||||
|
|
||||||
|
|
||||||
def ms_to_mmss(ms, decimals=0, negative=False):
|
|
||||||
if not ms:
|
if not ms:
|
||||||
return "-"
|
return "-"
|
||||||
sign = ""
|
sign = ""
|
||||||
@ -107,3 +161,66 @@ def ms_to_mmss(ms, decimals=0, negative=False):
|
|||||||
seconds = 59.0
|
seconds = 59.0
|
||||||
|
|
||||||
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
|
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
|
||||||
|
|
||||||
|
|
||||||
|
def open_in_audacity(path: str) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
Open passed file in Audacity
|
||||||
|
|
||||||
|
Return True if apparently opened successfully, else False
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Return if audacity not running
|
||||||
|
if "audacity" not in [i.name() for i in psutil.process_iter()]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Return if path not given
|
||||||
|
if not path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid())
|
||||||
|
from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid())
|
||||||
|
eol: str = '\n'
|
||||||
|
|
||||||
|
def send_command(command: str) -> None:
|
||||||
|
"""Send a single command."""
|
||||||
|
to_audacity.write(command + eol)
|
||||||
|
to_audacity.flush()
|
||||||
|
|
||||||
|
def get_response() -> str:
|
||||||
|
"""Return the command response."""
|
||||||
|
|
||||||
|
result: str = ''
|
||||||
|
line: str = ''
|
||||||
|
|
||||||
|
while True:
|
||||||
|
result += line
|
||||||
|
line = from_audacity.readline()
|
||||||
|
if line == '\n' and len(result) > 0:
|
||||||
|
break
|
||||||
|
return result
|
||||||
|
|
||||||
|
def do_command(command: str) -> str:
|
||||||
|
"""Send one command, and return the response."""
|
||||||
|
|
||||||
|
send_command(command)
|
||||||
|
response = get_response()
|
||||||
|
return response
|
||||||
|
|
||||||
|
with open(to_pipe, 'w') as to_audacity, open(
|
||||||
|
from_pipe, 'rt') as from_audacity:
|
||||||
|
do_command(f'Import2: Filename="{path}"')
|
||||||
|
|
||||||
|
|
||||||
|
def show_warning(title: str, msg: str) -> None:
|
||||||
|
"""Display a warning to user"""
|
||||||
|
|
||||||
|
QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)
|
||||||
|
|
||||||
|
|
||||||
|
def trailing_silence(
|
||||||
|
audio_segment: AudioSegment, silence_threshold: int = -50,
|
||||||
|
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
||||||
|
"""Return fade point from start in milliseconds"""
|
||||||
|
|
||||||
|
return fade_point(audio_segment, silence_threshold, chunk_size)
|
||||||
|
|||||||
10
app/log.py
10
app/log.py
@ -9,7 +9,7 @@ from config import Config
|
|||||||
|
|
||||||
|
|
||||||
class LevelTagFilter(logging.Filter):
|
class LevelTagFilter(logging.Filter):
|
||||||
"Add leveltag"
|
"""Add leveltag"""
|
||||||
|
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
# Extract the first character of the level name
|
# Extract the first character of the level name
|
||||||
@ -32,9 +32,9 @@ syslog = logging.handlers.SysLogHandler(address='/dev/log')
|
|||||||
syslog.setLevel(Config.LOG_LEVEL_SYSLOG)
|
syslog.setLevel(Config.LOG_LEVEL_SYSLOG)
|
||||||
|
|
||||||
# Filter
|
# Filter
|
||||||
filter = LevelTagFilter()
|
local_filter = LevelTagFilter()
|
||||||
syslog.addFilter(filter)
|
syslog.addFilter(local_filter)
|
||||||
stderr.addFilter(filter)
|
stderr.addFilter(local_filter)
|
||||||
|
|
||||||
# create formatter and add it to the handlers
|
# create formatter and add it to the handlers
|
||||||
stderr_fmt = logging.Formatter('[%(asctime)s] %(leveltag)s: %(message)s',
|
stderr_fmt = logging.Formatter('[%(asctime)s] %(leveltag)s: %(message)s',
|
||||||
@ -103,6 +103,6 @@ if __name__ == "__main__":
|
|||||||
return i()
|
return i()
|
||||||
|
|
||||||
def i():
|
def i():
|
||||||
1 / 0
|
return 1 / 0
|
||||||
|
|
||||||
f()
|
f()
|
||||||
|
|||||||
666
app/model.py
666
app/model.py
@ -1,666 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
import re
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from sqlalchemy import (
|
|
||||||
Boolean,
|
|
||||||
Column,
|
|
||||||
DateTime,
|
|
||||||
Float,
|
|
||||||
ForeignKey,
|
|
||||||
Integer,
|
|
||||||
String,
|
|
||||||
func
|
|
||||||
)
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
|
|
||||||
from sqlalchemy.orm import relationship, sessionmaker
|
|
||||||
|
|
||||||
from config import Config
|
|
||||||
from log import DEBUG, ERROR
|
|
||||||
|
|
||||||
# Create session at the global level as per
|
|
||||||
# https://docs.sqlalchemy.org/en/13/orm/session_basics.html
|
|
||||||
|
|
||||||
# Set up database connection
|
|
||||||
engine = sqlalchemy.create_engine(f"{Config.MYSQL_CONNECT}?charset=utf8",
|
|
||||||
encoding='utf-8',
|
|
||||||
echo=Config.DISPLAY_SQL,
|
|
||||||
pool_pre_ping=True)
|
|
||||||
Base = declarative_base()
|
|
||||||
Base.metadata.create_all(engine)
|
|
||||||
|
|
||||||
# Create a Session factory
|
|
||||||
Session = sessionmaker(bind=engine)
|
|
||||||
|
|
||||||
|
|
||||||
# Database classes
|
|
||||||
class NoteColours(Base):
|
|
||||||
__tablename__ = 'notecolours'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
substring = Column(String(256), index=False)
|
|
||||||
hexcolour = Column(String(6), index=False)
|
|
||||||
enabled = Column(Boolean, default=True, index=True)
|
|
||||||
is_regex = Column(Boolean, default=False, index=False)
|
|
||||||
is_casesensitive = Column(Boolean, default=False, index=False)
|
|
||||||
order = Column(Integer, index=True)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return (
|
|
||||||
f"<NoteColour(id={self.id}, substring={self.substring}, colour={self.hexcolour}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_colour(session, text):
|
|
||||||
"""
|
|
||||||
Parse text and return colour string if match, else None
|
|
||||||
|
|
||||||
Currently ignore is_regex and is_casesensitive
|
|
||||||
"""
|
|
||||||
|
|
||||||
for rec in (
|
|
||||||
session.query(NoteColours)
|
|
||||||
.filter(NoteColours.enabled == True)
|
|
||||||
.order_by(NoteColours.order)
|
|
||||||
.all()
|
|
||||||
):
|
|
||||||
if rec.is_regex:
|
|
||||||
if rec.is_casesensitive:
|
|
||||||
p = re.compile(rec.substring)
|
|
||||||
else:
|
|
||||||
p = re.compile(rec.substring, re.IGNORECASE)
|
|
||||||
if p.match(text):
|
|
||||||
return '#' + rec.hexcolour
|
|
||||||
else:
|
|
||||||
if rec.is_casesensitive:
|
|
||||||
if rec.substring in text:
|
|
||||||
return '#' + rec.hexcolour
|
|
||||||
else:
|
|
||||||
if rec.substring.lower() in text.lower():
|
|
||||||
return '#' + rec.hexcolour
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class Notes(Base):
|
|
||||||
__tablename__ = 'notes'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
playlist_id = Column(Integer, ForeignKey('playlists.id'))
|
|
||||||
playlist = relationship("Playlists", back_populates="notes")
|
|
||||||
row = Column(Integer, nullable=False)
|
|
||||||
note = Column(String(256), index=False)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return (
|
|
||||||
f"<Note(id={self.id}, row={self.row}, note={self.note}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def add_note(session, playlist_id, row, text):
|
|
||||||
"Add note"
|
|
||||||
|
|
||||||
DEBUG(f"add_note(playlist_id={playlist_id}, row={row}, text={text})")
|
|
||||||
note = Notes()
|
|
||||||
note.playlist_id = playlist_id
|
|
||||||
note.row = row
|
|
||||||
note.note = text
|
|
||||||
session.add(note)
|
|
||||||
session.commit()
|
|
||||||
return note
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def delete_note(session, id):
|
|
||||||
"Delete note"
|
|
||||||
|
|
||||||
DEBUG(f"delete_note(id={id}")
|
|
||||||
|
|
||||||
session.query(Notes).filter(Notes.id == id).delete()
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def update_note(cls, session, id, row, text=None):
|
|
||||||
"""
|
|
||||||
Update note details. If text=None, don't change text.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEBUG(f"Notes.update_note(id={id}, row={row}, text={text})")
|
|
||||||
|
|
||||||
note = session.query(cls).filter(cls.id == id).one()
|
|
||||||
note.row = row
|
|
||||||
if text:
|
|
||||||
note.note = text
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class Playdates(Base):
|
|
||||||
__tablename__ = 'playdates'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
lastplayed = Column(DateTime, index=True, default=None)
|
|
||||||
track_id = Column(Integer, ForeignKey('tracks.id'))
|
|
||||||
tracks = relationship("Tracks", back_populates="playdates")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def add_playdate(session, track):
|
|
||||||
"Record that track was played"
|
|
||||||
|
|
||||||
DEBUG(f"add_playdate(track={track})")
|
|
||||||
pd = Playdates()
|
|
||||||
pd.lastplayed = datetime.now()
|
|
||||||
pd.track_id = track.id
|
|
||||||
session.add(pd)
|
|
||||||
track.update_lastplayed(session, track.id)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def last_played(session, track_id):
|
|
||||||
"Return datetime track last played or None"
|
|
||||||
|
|
||||||
last_played = session.query(Playdates.lastplayed).filter(
|
|
||||||
(Playdates.track_id == track_id)
|
|
||||||
).order_by(Playdates.lastplayed.desc()).first()
|
|
||||||
if last_played:
|
|
||||||
return last_played[0]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def remove_track(session, track_id):
|
|
||||||
"""
|
|
||||||
Remove all records of track_id
|
|
||||||
"""
|
|
||||||
|
|
||||||
session.query(Playdates).filter(
|
|
||||||
Playdates.track_id == track_id,
|
|
||||||
).delete()
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class Playlists(Base):
|
|
||||||
"""
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
pl = session.query(Playlists).filter(Playlists.id == 1).one()
|
|
||||||
|
|
||||||
pl
|
|
||||||
<Playlist(id=1, name=Default>
|
|
||||||
|
|
||||||
pl.tracks
|
|
||||||
[<__main__.PlaylistTracks at 0x7fcd20181c18>,
|
|
||||||
<__main__.PlaylistTracks at 0x7fcd20181c88>,
|
|
||||||
<__main__.PlaylistTracks at 0x7fcd20181be0>,
|
|
||||||
<__main__.PlaylistTracks at 0x7fcd20181c50>]
|
|
||||||
|
|
||||||
[a.tracks for a in pl.tracks]
|
|
||||||
[<Track(id=3992, title=Yesterday Man, artist=Various, path=/h[...]
|
|
||||||
<Track(id=2238, title=These Boots Are Made For Walkin', arti[...]
|
|
||||||
<Track(id=3837, title=Babe, artist=Various, path=/home/kae/m[...]
|
|
||||||
<Track(id=2332, title=Such Great Heights - Remastered, artis[...]]
|
|
||||||
|
|
||||||
glue = PlaylistTracks(row=5)
|
|
||||||
|
|
||||||
tr = session.query(Tracks).filter(Tracks.id == 676).one()
|
|
||||||
|
|
||||||
tr
|
|
||||||
<Track(id=676, title=Seven Nation Army, artist=White Stripes,
|
|
||||||
path=/home/kae/music/White Stripes/Elephant/01. Seven Nation Army.flac>
|
|
||||||
|
|
||||||
glue.track_id = tr.id
|
|
||||||
|
|
||||||
pl.tracks.append(glue)
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
[a.tracks for a in pl.tracks]
|
|
||||||
[<Track(id=3992, title=Yesterday Man, artist=Various, path=/h[...]
|
|
||||||
<Track(id=2238, title=These Boots Are Made For Walkin', arti[...]
|
|
||||||
<Track(id=3837, title=Babe, artist=Various, path=/home/kae/m[...]
|
|
||||||
<Track(id=2332, title=Such Great Heights - Remastered, artis[...]
|
|
||||||
<Track(id=676, title=Seven Nation Army, artist=White Stripes[...]]
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "playlists"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
name = Column(String(32), nullable=False, unique=True)
|
|
||||||
last_used = Column(DateTime, default=None, nullable=True)
|
|
||||||
loaded = Column(Boolean, default=True)
|
|
||||||
notes = relationship("Notes",
|
|
||||||
order_by="Notes.row",
|
|
||||||
back_populates="playlist")
|
|
||||||
tracks = relationship("PlaylistTracks",
|
|
||||||
order_by="PlaylistTracks.row",
|
|
||||||
back_populates="playlists")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return (f"<Playlists(id={self.id}, name={self.name}>")
|
|
||||||
|
|
||||||
def add_track(self, session, track, row=None):
|
|
||||||
"""
|
|
||||||
Add track to playlist at given row.
|
|
||||||
If row=None, add to end of playlist
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not row:
|
|
||||||
row = PlaylistTracks.new_row(session, self.id)
|
|
||||||
|
|
||||||
DEBUG(f"Playlists:add_track({session=}, {track=}, {row=})")
|
|
||||||
|
|
||||||
glue = PlaylistTracks(row=row)
|
|
||||||
glue.track_id = track.id
|
|
||||||
self.tracks.append(glue)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
def close(self, session):
|
|
||||||
"Record playlist as no longer loaded"
|
|
||||||
|
|
||||||
self.loaded = False
|
|
||||||
session.add(self)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_all_closed_playlists(session):
|
|
||||||
"Returns a list of all playlists not currently open"
|
|
||||||
|
|
||||||
return (
|
|
||||||
session.query(Playlists)
|
|
||||||
.filter(
|
|
||||||
(Playlists.loaded == False) | # noqa E712
|
|
||||||
(Playlists.loaded == None)
|
|
||||||
)
|
|
||||||
.order_by(Playlists.last_used.desc())
|
|
||||||
).all()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_all_playlists(session):
|
|
||||||
"Returns a list of all playlists"
|
|
||||||
|
|
||||||
return session.query(Playlists).all()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_last_used(session):
|
|
||||||
"""
|
|
||||||
Return a list of playlists marked "loaded", ordered by loaded date.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return (
|
|
||||||
session.query(Playlists)
|
|
||||||
.filter(Playlists.loaded == True) # noqa E712
|
|
||||||
.order_by(Playlists.last_used.desc())
|
|
||||||
).all()
|
|
||||||
|
|
||||||
def get_notes(self):
|
|
||||||
return [a.note for a in self.notes]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_playlist(session, playlist_id):
|
|
||||||
return (
|
|
||||||
session.query(Playlists)
|
|
||||||
.filter(
|
|
||||||
Playlists.id == playlist_id # noqa E712
|
|
||||||
)
|
|
||||||
).one()
|
|
||||||
|
|
||||||
def get_tracks(self):
|
|
||||||
return [a.tracks for a in self.tracks]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def new(session, name):
|
|
||||||
DEBUG(f"Playlists.new(name={name})")
|
|
||||||
playlist = Playlists()
|
|
||||||
playlist.name = name
|
|
||||||
session.add(playlist)
|
|
||||||
session.commit()
|
|
||||||
return playlist
|
|
||||||
|
|
||||||
def open(self, session):
|
|
||||||
"Mark playlist as loaded and used now"
|
|
||||||
|
|
||||||
self.loaded = True
|
|
||||||
self.last_used = datetime.now()
|
|
||||||
session.add(self)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistTracks(Base):
|
|
||||||
__tablename__ = 'playlisttracks'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
playlist_id = Column(Integer, ForeignKey('playlists.id'), primary_key=True)
|
|
||||||
track_id = Column(Integer, ForeignKey('tracks.id'), primary_key=True)
|
|
||||||
row = Column(Integer, nullable=False)
|
|
||||||
tracks = relationship("Tracks", back_populates="playlists")
|
|
||||||
playlists = relationship("Playlists", back_populates="tracks")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def add_track(session, playlist_id, track_id, row):
|
|
||||||
DEBUG(
|
|
||||||
f"PlaylistTracks.add_track(playlist_id={playlist_id}, "
|
|
||||||
f"track_id={track_id}, row={row})"
|
|
||||||
)
|
|
||||||
plt = PlaylistTracks()
|
|
||||||
plt.playlist_id = playlist_id,
|
|
||||||
plt.track_id = track_id,
|
|
||||||
plt.row = row
|
|
||||||
session.add(plt)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_track_playlists(session, track_id):
|
|
||||||
"Return all PlaylistTracks objects with this track_id"
|
|
||||||
|
|
||||||
return session.query(PlaylistTracks).filter(
|
|
||||||
PlaylistTracks.track_id == track_id).all()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def move_track(session, from_playlist_id, row, to_playlist_id):
|
|
||||||
DEBUG(
|
|
||||||
"PlaylistTracks.move_tracks(from_playlist_id="
|
|
||||||
f"{from_playlist_id}, row={row}, "
|
|
||||||
f"to_playlist_id={to_playlist_id})"
|
|
||||||
)
|
|
||||||
max_row = session.query(func.max(PlaylistTracks.row)).filter(
|
|
||||||
PlaylistTracks.playlist_id == to_playlist_id).scalar()
|
|
||||||
if max_row is None:
|
|
||||||
# Destination playlist is empty; use row 0
|
|
||||||
new_row = 0
|
|
||||||
else:
|
|
||||||
# Destination playlist has tracks; add to end
|
|
||||||
new_row = max_row + 1
|
|
||||||
try:
|
|
||||||
record = session.query(PlaylistTracks).filter(
|
|
||||||
PlaylistTracks.playlist_id == from_playlist_id,
|
|
||||||
PlaylistTracks.row == row).one()
|
|
||||||
except NoResultFound:
|
|
||||||
# Issue #38?
|
|
||||||
ERROR(
|
|
||||||
f"No rows matched in query: "
|
|
||||||
f"PlaylistTracks.playlist_id == {from_playlist_id}, "
|
|
||||||
f"PlaylistTracks.row == {row}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
record.playlist_id = to_playlist_id
|
|
||||||
record.row = new_row
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def new_row(session, playlist_id):
|
|
||||||
"Return row number > largest existing row number"
|
|
||||||
|
|
||||||
last_row = session.query(func.max(PlaylistTracks.row)).one()[0]
|
|
||||||
return last_row + 1
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def remove_all_tracks(session, playlist_id):
|
|
||||||
"""
|
|
||||||
Remove all tracks from passed playlist_id
|
|
||||||
"""
|
|
||||||
|
|
||||||
session.query(PlaylistTracks).filter(
|
|
||||||
PlaylistTracks.playlist_id == playlist_id,
|
|
||||||
).delete()
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def remove_track(session, playlist_id, row):
|
|
||||||
DEBUG(
|
|
||||||
f"PlaylistTracks.remove_track(playlist_id={playlist_id}, "
|
|
||||||
f"row={row})"
|
|
||||||
)
|
|
||||||
session.query(PlaylistTracks).filter(
|
|
||||||
PlaylistTracks.playlist_id == playlist_id,
|
|
||||||
PlaylistTracks.row == row
|
|
||||||
).delete()
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def update_row_track(session, playlist_id, row, track_id):
|
|
||||||
DEBUG(
|
|
||||||
f"PlaylistTracks.update_track_row(playlist_id={playlist_id}, "
|
|
||||||
f"row={row}, track_id={track_id})"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
plt = session.query(PlaylistTracks).filter(
|
|
||||||
PlaylistTracks.playlist_id == playlist_id,
|
|
||||||
PlaylistTracks.row == row
|
|
||||||
).one()
|
|
||||||
except MultipleResultsFound:
|
|
||||||
ERROR(
|
|
||||||
f"Multiple rows matched in query: "
|
|
||||||
f"PlaylistTracks.playlist_id == {playlist_id}, "
|
|
||||||
f"PlaylistTracks.row == {row}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
except NoResultFound:
|
|
||||||
ERROR(
|
|
||||||
f"No rows matched in query: "
|
|
||||||
f"PlaylistTracks.playlist_id == {playlist_id}, "
|
|
||||||
f"PlaylistTracks.row == {row}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
plt.track_id = track_id
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(Base):
|
|
||||||
__tablename__ = 'settings'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
name = Column(String(32), nullable=False, unique=True)
|
|
||||||
f_datetime = Column(DateTime, default=None, nullable=True)
|
|
||||||
f_int = Column(Integer, default=None, nullable=True)
|
|
||||||
f_string = Column(String(128), default=None, nullable=True)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_int(cls, session, name):
|
|
||||||
try:
|
|
||||||
int_setting = session.query(cls).filter(
|
|
||||||
cls.name == name).one()
|
|
||||||
except NoResultFound:
|
|
||||||
int_setting = Settings()
|
|
||||||
int_setting.name = name
|
|
||||||
int_setting.f_int = None
|
|
||||||
session.add(int_setting)
|
|
||||||
session.commit()
|
|
||||||
return int_setting
|
|
||||||
|
|
||||||
def update(self, session, data):
|
|
||||||
for key, value in data.items():
|
|
||||||
assert hasattr(self, key)
|
|
||||||
setattr(self, key, value)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class Tracks(Base):
|
|
||||||
__tablename__ = 'tracks'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
title = Column(String(256), index=True)
|
|
||||||
artist = Column(String(256), index=True)
|
|
||||||
duration = Column(Integer, index=True)
|
|
||||||
start_gap = Column(Integer, index=False)
|
|
||||||
fade_at = Column(Integer, index=False)
|
|
||||||
silence_at = Column(Integer, index=False)
|
|
||||||
path = Column(String(2048), index=False, nullable=False)
|
|
||||||
mtime = Column(Float, index=True)
|
|
||||||
lastplayed = Column(DateTime, index=True, default=None)
|
|
||||||
playlists = relationship("PlaylistTracks", back_populates="tracks")
|
|
||||||
playdates = relationship("Playdates", back_populates="tracks")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return (
|
|
||||||
f"<Track(id={self.id}, title={self.title}, "
|
|
||||||
f"artist={self.artist}, path={self.path}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Not currently used 1 June 2021
|
|
||||||
# @staticmethod
|
|
||||||
# def get_note(session, id):
|
|
||||||
# return session.query(Notes).filter(Notes.id == id).one()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_all_paths(session):
|
|
||||||
"Return a list of paths of all tracks"
|
|
||||||
|
|
||||||
return [a[0] for a in session.query(Tracks.path).all()]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_all_tracks(session):
|
|
||||||
"Return a list of all tracks"
|
|
||||||
|
|
||||||
return session.query(Tracks).all()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_or_create(cls, session, path):
|
|
||||||
DEBUG(f"Tracks.get_or_create(path={path})")
|
|
||||||
try:
|
|
||||||
track = session.query(cls).filter(cls.path == path).one()
|
|
||||||
except NoResultFound:
|
|
||||||
track = Tracks()
|
|
||||||
track.path = path
|
|
||||||
session.add(track)
|
|
||||||
return track
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_duration(session, id):
|
|
||||||
try:
|
|
||||||
return session.query(
|
|
||||||
Tracks.duration).filter(Tracks.id == id).one()[0]
|
|
||||||
except NoResultFound:
|
|
||||||
ERROR(f"Can't find track id {id}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_track_from_filename(session, filename):
|
|
||||||
"""
|
|
||||||
Return track if one and only one track in database has passed
|
|
||||||
filename (ie, basename of path). Return None if zero or more
|
|
||||||
than one track matches.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEBUG(f"Tracks.get_track_from_filename({filename=})")
|
|
||||||
try:
|
|
||||||
track = session.query(Tracks).filter(Tracks.path.ilike(
|
|
||||||
f'%{os.path.sep}{filename}')).one()
|
|
||||||
return track
|
|
||||||
except (NoResultFound, MultipleResultsFound):
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_track_from_path(session, path):
|
|
||||||
"""
|
|
||||||
Return track with passee path, or None.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEBUG(f"Tracks.get_track_from_path({path=})")
|
|
||||||
|
|
||||||
return session.query(Tracks).filter(Tracks.path == path).first()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_path(session, track_id):
|
|
||||||
"Return path of passed track_id, or None"
|
|
||||||
|
|
||||||
try:
|
|
||||||
return session.query(Tracks.path).filter(
|
|
||||||
Tracks.id == track_id).one()[0]
|
|
||||||
except NoResultFound:
|
|
||||||
ERROR(f"Can't find track id {track_id}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_track(session, track_id):
|
|
||||||
"Return track or None"
|
|
||||||
|
|
||||||
try:
|
|
||||||
DEBUG(f"Tracks.get_track(track_id={track_id})")
|
|
||||||
track = session.query(Tracks).filter(Tracks.id == track_id).one()
|
|
||||||
return track
|
|
||||||
except NoResultFound:
|
|
||||||
ERROR(f"get_track({track_id}): not found")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def remove_path(session, path):
|
|
||||||
"Remove track with passed path from database"
|
|
||||||
|
|
||||||
DEBUG(f"Tracks.remove_path({path=})")
|
|
||||||
|
|
||||||
try:
|
|
||||||
session.query(Tracks).filter(Tracks.path == path).delete()
|
|
||||||
session.commit()
|
|
||||||
except IntegrityError as exception:
|
|
||||||
ERROR(f"Can't remove track with {path=} ({exception=})")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def search(session, title=None, artist=None, duration=None):
|
|
||||||
"""
|
|
||||||
Return any tracks matching passed criteria
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEBUG(
|
|
||||||
f"Tracks.search({title=}, {artist=}), {duration=})"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not title and not artist and not duration:
|
|
||||||
return None
|
|
||||||
|
|
||||||
q = session.query(Tracks).filter(False)
|
|
||||||
if title:
|
|
||||||
q = q.filter(Tracks.title == title)
|
|
||||||
if artist:
|
|
||||||
q = q.filter(Tracks.artist == artist)
|
|
||||||
if duration:
|
|
||||||
q = q.filter(Tracks.duration == duration)
|
|
||||||
|
|
||||||
return q.all()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def search_artists(session, text):
|
|
||||||
return (
|
|
||||||
session.query(Tracks)
|
|
||||||
.filter(Tracks.artist.ilike(f"%{text}%"))
|
|
||||||
.order_by(Tracks.title)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def search_titles(session, text):
|
|
||||||
return (
|
|
||||||
session.query(Tracks)
|
|
||||||
.filter(Tracks.title.ilike(f"%{text}%"))
|
|
||||||
.order_by(Tracks.title)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def track_from_id(session, id):
|
|
||||||
return session.query(Tracks).filter(
|
|
||||||
Tracks.id == id).one()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def update_lastplayed(session, track_id):
|
|
||||||
track = session.query(Tracks).filter(Tracks.id == track_id).one()
|
|
||||||
track.lastplayed = datetime.now()
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def update_artist(session, track_id, artist):
|
|
||||||
track = session.query(Tracks).filter(Tracks.id == track_id).one()
|
|
||||||
track.artist = artist
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def update_title(session, track_id, title):
|
|
||||||
track = session.query(Tracks).filter(Tracks.id == track_id).one()
|
|
||||||
track.title = title
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
def update_path(self, newpath):
|
|
||||||
self.path = newpath
|
|
||||||
648
app/models.py
Normal file
648
app/models.py
Normal file
@ -0,0 +1,648 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
|
||||||
|
from dbconfig import Session
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydub import AudioSegment
|
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
|
||||||
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
|
Column,
|
||||||
|
DateTime,
|
||||||
|
Float,
|
||||||
|
ForeignKey,
|
||||||
|
func,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
UniqueConstraint,
|
||||||
|
)
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.orm import (
|
||||||
|
backref,
|
||||||
|
relationship,
|
||||||
|
RelationshipProperty
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm.collections import attribute_mapped_collection
|
||||||
|
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
from helpers import (
|
||||||
|
fade_point,
|
||||||
|
get_audio_segment,
|
||||||
|
leading_silence,
|
||||||
|
trailing_silence,
|
||||||
|
)
|
||||||
|
from log import DEBUG, ERROR
|
||||||
|
|
||||||
|
Base: DeclarativeMeta = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
# Database classes
|
||||||
|
class NoteColours(Base):
|
||||||
|
__tablename__ = 'notecolours'
|
||||||
|
|
||||||
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
substring: str = Column(String(256), index=False)
|
||||||
|
colour: str = Column(String(21), index=False)
|
||||||
|
enabled: bool = Column(Boolean, default=True, index=True)
|
||||||
|
is_regex: bool = Column(Boolean, default=False, index=False)
|
||||||
|
is_casesensitive: bool = Column(Boolean, default=False, index=False)
|
||||||
|
order: int = Column(Integer, index=True)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, session: Session, substring: str, colour: str,
|
||||||
|
enabled: bool = True, is_regex: bool = False,
|
||||||
|
is_casesensitive: bool = False, order: int = 0) -> None:
|
||||||
|
self.substring = substring
|
||||||
|
self.colour = colour
|
||||||
|
self.enabled = enabled
|
||||||
|
self.is_regex = is_regex
|
||||||
|
self.is_casesensitive = is_casesensitive
|
||||||
|
self.order = order
|
||||||
|
|
||||||
|
session.add(self)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<NoteColour(id={self.id}, substring={self.substring}, "
|
||||||
|
f"colour={self.colour}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all(cls, session: Session) -> Optional[List["NoteColours"]]:
|
||||||
|
"""Return all records"""
|
||||||
|
|
||||||
|
return session.query(cls).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_id(cls, session: Session, note_id: int) -> \
|
||||||
|
Optional["NoteColours"]:
|
||||||
|
"""Return record identified by id, or None if not found"""
|
||||||
|
|
||||||
|
return session.query(NoteColours).filter(
|
||||||
|
NoteColours.id == note_id).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_colour(session: Session, text: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Parse text and return colour string if matched, else None
|
||||||
|
"""
|
||||||
|
|
||||||
|
for rec in (
|
||||||
|
session.query(NoteColours)
|
||||||
|
.filter(NoteColours.enabled.is_(True))
|
||||||
|
.order_by(NoteColours.order)
|
||||||
|
.all()
|
||||||
|
):
|
||||||
|
if rec.is_regex:
|
||||||
|
flags = re.UNICODE
|
||||||
|
if not rec.is_casesensitive:
|
||||||
|
flags |= re.IGNORECASE
|
||||||
|
p = re.compile(rec.substring, flags)
|
||||||
|
if p.match(text):
|
||||||
|
return rec.colour
|
||||||
|
else:
|
||||||
|
if rec.is_casesensitive:
|
||||||
|
if rec.substring in text:
|
||||||
|
return rec.colour
|
||||||
|
else:
|
||||||
|
if rec.substring.lower() in text.lower():
|
||||||
|
return rec.colour
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Notes(Base):
|
||||||
|
__tablename__ = 'notes'
|
||||||
|
|
||||||
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
playlist_id: int = Column(Integer, ForeignKey('playlists.id'))
|
||||||
|
playlist: RelationshipProperty = relationship(
|
||||||
|
"Playlists", back_populates="notes", lazy="joined")
|
||||||
|
row: int = Column(Integer, nullable=False)
|
||||||
|
note: str = Column(String(256), index=False)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, session: Session, playlist_id: int, row: int, text: str) -> None:
|
||||||
|
"""Create note"""
|
||||||
|
|
||||||
|
DEBUG(f"Notes.__init__({playlist_id=}, {row=}, {text=})")
|
||||||
|
self.playlist_id = playlist_id
|
||||||
|
self.row = row
|
||||||
|
self.note = text
|
||||||
|
session.add(self)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Note(id={self.id}, row={self.row}, note={self.note}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_note(self, session: Session) -> None:
|
||||||
|
"""Delete note"""
|
||||||
|
|
||||||
|
DEBUG(f"delete_note({self.id=}")
|
||||||
|
|
||||||
|
session.query(Notes).filter_by(id=self.id).delete()
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_id(cls, session: Session, note_id: int) -> Optional["Notes"]:
|
||||||
|
"""Return note or None"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
DEBUG(f"Notes.get_track(track_id={note_id})")
|
||||||
|
note = session.query(cls).filter(cls.id == note_id).one()
|
||||||
|
return note
|
||||||
|
except NoResultFound:
|
||||||
|
ERROR(f"get_track({note_id}): not found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_note(
|
||||||
|
self, session: Session, row: int,
|
||||||
|
text: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Update note details. If text=None, don't change text.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEBUG(f"Notes.update_note({self.id=}, {row=}, {text=})")
|
||||||
|
|
||||||
|
self.row = row
|
||||||
|
if text:
|
||||||
|
self.note = text
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class Playdates(Base):
|
||||||
|
__tablename__ = 'playdates'
|
||||||
|
|
||||||
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
lastplayed: datetime = Column(DateTime, index=True, default=None)
|
||||||
|
track_id: int = Column(Integer, ForeignKey('tracks.id'))
|
||||||
|
tracks: RelationshipProperty = relationship(
|
||||||
|
"Tracks", back_populates="playdates", lazy="joined")
|
||||||
|
|
||||||
|
def __init__(self, session: Session, track_id: int) -> None:
|
||||||
|
"""Record that track was played"""
|
||||||
|
|
||||||
|
DEBUG(f"add_playdate({track_id=})")
|
||||||
|
|
||||||
|
self.lastplayed = datetime.now()
|
||||||
|
self.track_id = track_id
|
||||||
|
session.add(self)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def last_played(session: Session, track_id: int) -> Optional[datetime]:
|
||||||
|
"""Return datetime track last played or None"""
|
||||||
|
|
||||||
|
last_played: Optional[Playdates] = session.query(
|
||||||
|
Playdates.lastplayed).filter(
|
||||||
|
(Playdates.track_id == track_id)
|
||||||
|
).order_by(Playdates.lastplayed.desc()).first()
|
||||||
|
if last_played:
|
||||||
|
return last_played[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_track(session: Session, track_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Remove all records of track_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
session.query(Playdates).filter(
|
||||||
|
Playdates.track_id == track_id).delete()
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class Playlists(Base):
|
||||||
|
"""
|
||||||
|
Manage playlists
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "playlists"
|
||||||
|
|
||||||
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
name: str = Column(String(32), nullable=False, unique=True)
|
||||||
|
last_used: datetime = Column(DateTime, default=None, nullable=True)
|
||||||
|
loaded: bool = Column(Boolean, default=True, nullable=False)
|
||||||
|
notes = relationship(
|
||||||
|
"Notes", order_by="Notes.row",
|
||||||
|
back_populates="playlist", lazy="joined"
|
||||||
|
)
|
||||||
|
|
||||||
|
tracks = association_proxy('playlist_tracks', 'tracks')
|
||||||
|
row = association_proxy('playlist_tracks', 'row')
|
||||||
|
|
||||||
|
def __init__(self, session: Session, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
session.add(self)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Playlists(id={self.id}, name={self.name}>"
|
||||||
|
|
||||||
|
def add_note(self, session: Session, row: int, text: str) -> Notes:
|
||||||
|
"""Add note to playlist at passed row"""
|
||||||
|
|
||||||
|
return Notes(session, self.id, row, text)
|
||||||
|
|
||||||
|
def add_track(
|
||||||
|
self, session: Session, track_id: int,
|
||||||
|
row: Optional[int] = None) -> None:
|
||||||
|
"""
|
||||||
|
Add track to playlist at given row.
|
||||||
|
If row=None, add to end of playlist
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
row = PlaylistTracks.next_free_row(session, self.id)
|
||||||
|
|
||||||
|
PlaylistTracks(session, self.id, track_id, row)
|
||||||
|
|
||||||
|
def close(self, session: Session) -> None:
|
||||||
|
"""Record playlist as no longer loaded"""
|
||||||
|
|
||||||
|
self.loaded = False
|
||||||
|
session.add(self)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all(cls, session: Session) -> List["Playlists"]:
|
||||||
|
"""Returns a list of all playlists ordered by last use"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
session.query(cls).order_by(cls.last_used.desc())
|
||||||
|
).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_id(cls, session: Session, playlist_id: int) -> "Playlists":
|
||||||
|
return (session.query(cls).filter(cls.id == playlist_id)).one()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_closed(cls, session: Session) -> List["Playlists"]:
|
||||||
|
"""Returns a list of all closed playlists ordered by last use"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
session.query(cls)
|
||||||
|
.filter(cls.loaded.is_(False))
|
||||||
|
.order_by(cls.last_used.desc())
|
||||||
|
).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_open(cls, session: Session) -> List["Playlists"]:
|
||||||
|
"""
|
||||||
|
Return a list of playlists marked "loaded", ordered by loaded date.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
session.query(cls)
|
||||||
|
.filter(cls.loaded.is_(True))
|
||||||
|
.order_by(cls.last_used.desc())
|
||||||
|
).all()
|
||||||
|
|
||||||
|
def mark_open(self, session: Session) -> None:
|
||||||
|
"""Mark playlist as loaded and used now"""
|
||||||
|
|
||||||
|
self.loaded = True
|
||||||
|
self.last_used = datetime.now()
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def move_track(
|
||||||
|
self, session: Session, rows: List[int],
|
||||||
|
to_playlist: "Playlists") -> None:
|
||||||
|
"""Move tracks to another playlist"""
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
track = self.tracks[row]
|
||||||
|
to_playlist.add_track(session, track.id)
|
||||||
|
del self.tracks[row]
|
||||||
|
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def remove_all_tracks(self, session: Session) -> None:
|
||||||
|
"""
|
||||||
|
Remove all tracks from this playlist
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.tracks = {}
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def remove_track(self, session: Session, row: int) -> None:
|
||||||
|
DEBUG(f"Playlist.remove_track({self.id=}, {row=})")
|
||||||
|
|
||||||
|
# Get tracks collection for this playlist
|
||||||
|
tracks_collections = self.tracks
|
||||||
|
# Tracks are a dictionary of tracks keyed on row
|
||||||
|
# number. Remove the relevant row.
|
||||||
|
del tracks_collections[row]
|
||||||
|
# Save the new tracks collection
|
||||||
|
self.tracks = tracks_collections
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistTracks(Base):
|
||||||
|
__tablename__ = 'playlist_tracks'
|
||||||
|
|
||||||
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
playlist_id: int = Column(Integer, ForeignKey('playlists.id'),
|
||||||
|
primary_key=True)
|
||||||
|
track_id: int = Column(Integer, ForeignKey('tracks.id'), primary_key=True)
|
||||||
|
row: int = Column(Integer, nullable=False)
|
||||||
|
tracks: RelationshipProperty = relationship("Tracks")
|
||||||
|
playlist: RelationshipProperty = relationship(
|
||||||
|
Playlists,
|
||||||
|
backref=backref(
|
||||||
|
"playlist_tracks",
|
||||||
|
collection_class=attribute_mapped_collection("row"),
|
||||||
|
lazy="joined",
|
||||||
|
cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Ensure row numbers are unique within each playlist
|
||||||
|
__table_args__ = (UniqueConstraint
|
||||||
|
('row', 'playlist_id', name="uniquerow"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, session: Session, playlist_id: int, track_id: int,
|
||||||
|
row: int) -> None:
|
||||||
|
DEBUG(f"PlaylistTracks.__init__({playlist_id=}, {track_id=}, {row=})")
|
||||||
|
|
||||||
|
self.playlist_id = playlist_id
|
||||||
|
self.track_id = track_id
|
||||||
|
self.row = row
|
||||||
|
session.add(self)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def next_free_row(session: Session, playlist_id: int) -> int:
|
||||||
|
"""Return next free row number"""
|
||||||
|
|
||||||
|
row: int
|
||||||
|
|
||||||
|
last_row = session.query(
|
||||||
|
func.max(PlaylistTracks.row)
|
||||||
|
).filter_by(playlist_id=playlist_id).first()
|
||||||
|
# if there are no rows, the above returns (None, ) which is True
|
||||||
|
if last_row and last_row[0] is not None:
|
||||||
|
row = last_row[0] + 1
|
||||||
|
else:
|
||||||
|
row = 0
|
||||||
|
|
||||||
|
return row
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def move_rows(
|
||||||
|
session: Session, rows: List[int], from_playlist_id: int,
|
||||||
|
to_playlist_id: int) -> None:
|
||||||
|
"""Move rows between playlists"""
|
||||||
|
|
||||||
|
# A constraint deliberately blocks duplicate (playlist_id, row)
|
||||||
|
# entries in database; however, unallocated rows in the database
|
||||||
|
# are fine (ie, we can have rows 1, 4, 6 and no 2, 3, 5).
|
||||||
|
# Unallocated rows will be automatically removed when the
|
||||||
|
# playlist is saved.
|
||||||
|
|
||||||
|
lowest_source_row: int = min(rows)
|
||||||
|
first_destination_free_row = PlaylistTracks.next_free_row(
|
||||||
|
session, to_playlist_id)
|
||||||
|
# Calculate offset that will put the lowest row number being
|
||||||
|
# moved at the first free row in destination playlist
|
||||||
|
offset = first_destination_free_row - lowest_source_row
|
||||||
|
|
||||||
|
session.query(PlaylistTracks).filter(
|
||||||
|
PlaylistTracks.playlist_id == from_playlist_id,
|
||||||
|
PlaylistTracks.row.in_(rows)
|
||||||
|
).update({'playlist_id': to_playlist_id,
|
||||||
|
'row': PlaylistTracks.row + offset},
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(Base):
|
||||||
|
__tablename__ = 'settings'
|
||||||
|
|
||||||
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
name: str = Column(String(32), nullable=False, unique=True)
|
||||||
|
f_datetime: datetime = Column(DateTime, default=None, nullable=True)
|
||||||
|
f_int: int = Column(Integer, default=None, nullable=True)
|
||||||
|
f_string: str = Column(String(128), default=None, nullable=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_int_settings(cls, session: Session, name: str) -> "Settings":
|
||||||
|
"""Get setting for an integer or return new setting record"""
|
||||||
|
|
||||||
|
int_setting: Settings
|
||||||
|
|
||||||
|
try:
|
||||||
|
int_setting = session.query(cls).filter(
|
||||||
|
cls.name == name).one()
|
||||||
|
except NoResultFound:
|
||||||
|
int_setting = Settings()
|
||||||
|
int_setting.name = name
|
||||||
|
int_setting.f_int = None
|
||||||
|
session.add(int_setting)
|
||||||
|
session.flush()
|
||||||
|
return int_setting
|
||||||
|
|
||||||
|
def update(self, session: Session, data):
|
||||||
|
for key, value in data.items():
|
||||||
|
assert hasattr(self, key)
|
||||||
|
setattr(self, key, value)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class Tracks(Base):
|
||||||
|
__tablename__ = 'tracks'
|
||||||
|
|
||||||
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
title: str = Column(String(256), index=True)
|
||||||
|
artist: str = Column(String(256), index=True)
|
||||||
|
duration: int = Column(Integer, index=True)
|
||||||
|
start_gap: int = Column(Integer, index=False)
|
||||||
|
fade_at: int = Column(Integer, index=False)
|
||||||
|
silence_at: int = Column(Integer, index=False)
|
||||||
|
path: str = Column(String(2048), index=False, nullable=False)
|
||||||
|
mtime: float = Column(Float, index=True)
|
||||||
|
lastplayed: datetime = Column(DateTime, index=True, default=None)
|
||||||
|
playlists: RelationshipProperty = relationship("PlaylistTracks",
|
||||||
|
back_populates="tracks",
|
||||||
|
lazy="joined")
|
||||||
|
playdates: RelationshipProperty = relationship("Playdates",
|
||||||
|
back_populates="tracks",
|
||||||
|
lazy="joined")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
session: Session,
|
||||||
|
path: str,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
artist: Optional[str] = None,
|
||||||
|
duration: Optional[int] = None,
|
||||||
|
start_gap: Optional[int] = None,
|
||||||
|
fade_at: Optional[int] = None,
|
||||||
|
silence_at: Optional[int] = None,
|
||||||
|
mtime: Optional[float] = None,
|
||||||
|
lastplayed: Optional[datetime] = None,
|
||||||
|
) -> None:
|
||||||
|
self.path = path
|
||||||
|
self.title = title
|
||||||
|
self.artist = artist
|
||||||
|
self.duration = duration
|
||||||
|
self.start_gap = start_gap
|
||||||
|
self.fade_at = fade_at
|
||||||
|
self.silence_at = silence_at
|
||||||
|
self.mtime = mtime
|
||||||
|
self.lastplayed = lastplayed
|
||||||
|
|
||||||
|
session.add(self)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Track(id={self.id}, title={self.title}, "
|
||||||
|
f"artist={self.artist}, path={self.path}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_paths(session) -> List[str]:
|
||||||
|
"""Return a list of paths of all tracks"""
|
||||||
|
|
||||||
|
return [a[0] for a in session.query(Tracks.path).all()]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_tracks(cls, session: Session) -> List["Tracks"]:
|
||||||
|
"""Return a list of all tracks"""
|
||||||
|
|
||||||
|
return session.query(cls).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create(cls, session: Session, path: str) -> "Tracks":
|
||||||
|
"""
|
||||||
|
If a track with path exists, return it;
|
||||||
|
else created new track and return it
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEBUG(f"Tracks.get_or_create({path=})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
track = session.query(cls).filter(cls.path == path).one()
|
||||||
|
except NoResultFound:
|
||||||
|
track = Tracks(session, path)
|
||||||
|
|
||||||
|
return track
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_filename(cls, session: Session, filename: str) \
|
||||||
|
-> Optional["Tracks"]:
|
||||||
|
"""
|
||||||
|
Return track if one and only one track in database has passed
|
||||||
|
filename (ie, basename of path). Return None if zero or more
|
||||||
|
than one track matches.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEBUG(f"Tracks.get_track_from_filename({filename=})")
|
||||||
|
try:
|
||||||
|
track = session.query(Tracks).filter(Tracks.path.ilike(
|
||||||
|
f'%{os.path.sep}{filename}')).one()
|
||||||
|
return track
|
||||||
|
except (NoResultFound, MultipleResultsFound):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_path(cls, session: Session, path: str) -> List["Tracks"]:
|
||||||
|
"""
|
||||||
|
Return track with passee path, or None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEBUG(f"Tracks.get_track_from_path({path=})")
|
||||||
|
|
||||||
|
return session.query(Tracks).filter(Tracks.path == path).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_id(cls, session: Session, track_id: int) -> Optional["Tracks"]:
|
||||||
|
"""Return track or None"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
DEBUG(f"Tracks.get_track(track_id={track_id})")
|
||||||
|
track = session.query(Tracks).filter(Tracks.id == track_id).one()
|
||||||
|
return track
|
||||||
|
except NoResultFound:
|
||||||
|
ERROR(f"get_track({track_id}): not found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def rescan(self, session: Session) -> None:
|
||||||
|
"""
|
||||||
|
Update audio metadata for passed track.
|
||||||
|
"""
|
||||||
|
|
||||||
|
audio: AudioSegment = get_audio_segment(self.path)
|
||||||
|
self.duration = len(audio)
|
||||||
|
self.fade_at = round(fade_point(audio) / 1000,
|
||||||
|
Config.MILLISECOND_SIGFIGS) * 1000
|
||||||
|
self.mtime = os.path.getmtime(self.path)
|
||||||
|
self.silence_at = round(trailing_silence(audio) / 1000,
|
||||||
|
Config.MILLISECOND_SIGFIGS) * 1000
|
||||||
|
self.start_gap = leading_silence(audio)
|
||||||
|
session.add(self)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_by_path(session: Session, path: str) -> None:
|
||||||
|
"""Remove track with passed path from database"""
|
||||||
|
|
||||||
|
DEBUG(f"Tracks.remove_path({path=})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
session.query(Tracks).filter(Tracks.path == path).delete()
|
||||||
|
session.flush()
|
||||||
|
except IntegrityError as exception:
|
||||||
|
ERROR(f"Can't remove track with {path=} ({exception=})")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def search_artists(cls, session: Session, text: str) -> List["Tracks"]:
|
||||||
|
|
||||||
|
return (
|
||||||
|
session.query(cls)
|
||||||
|
.filter(cls.artist.ilike(f"%{text}%"))
|
||||||
|
.order_by(cls.title)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def search_titles(cls, session: Session, text: str) -> List["Tracks"]:
|
||||||
|
return (
|
||||||
|
session.query(cls)
|
||||||
|
.filter(cls.title.ilike(f"%{text}%"))
|
||||||
|
.order_by(cls.title)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_lastplayed(session: Session, track_id: int) -> None:
|
||||||
|
"""Update the last_played field to current datetime"""
|
||||||
|
|
||||||
|
rec = session.query(Tracks).get(track_id)
|
||||||
|
rec.lastplayed = datetime.now()
|
||||||
|
session.add(rec)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def update_artist(self, session: Session, artist: str) -> None:
|
||||||
|
self.artist = artist
|
||||||
|
session.add(self)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def update_title(self, session: Session, title: str) -> None:
|
||||||
|
self.title = title
|
||||||
|
session.add(self)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def update_path(self, newpath: str) -> None:
|
||||||
|
self.path = newpath
|
||||||
17
app/music.py
17
app/music.py
@ -81,18 +81,16 @@ class Music:
|
|||||||
sleep(sleep_time)
|
sleep(sleep_time)
|
||||||
|
|
||||||
with lock:
|
with lock:
|
||||||
DEBUG(f"music._facde(), stopping {p=}", True)
|
DEBUG(f"music._fade(), stopping {p=}", True)
|
||||||
|
|
||||||
p.stop()
|
p.stop()
|
||||||
DEBUG(f"Releasing player {p=}", True)
|
DEBUG(f"Releasing player {p=}", True)
|
||||||
p.release()
|
p.release()
|
||||||
# Ensure we don't reference player after release
|
|
||||||
p = None
|
|
||||||
|
|
||||||
self.fading -= 1
|
self.fading -= 1
|
||||||
|
|
||||||
def get_playtime(self):
|
def get_playtime(self):
|
||||||
"Return elapsed play time"
|
"""Return elapsed play time"""
|
||||||
|
|
||||||
with lock:
|
with lock:
|
||||||
if not self.player:
|
if not self.player:
|
||||||
@ -101,11 +99,14 @@ class Music:
|
|||||||
return self.player.get_time()
|
return self.player.get_time()
|
||||||
|
|
||||||
def get_position(self):
|
def get_position(self):
|
||||||
"Return current position"
|
"""Return current position"""
|
||||||
|
|
||||||
with lock:
|
with lock:
|
||||||
DEBUG("music.get_position", True)
|
DEBUG("music.get_position", True)
|
||||||
|
|
||||||
|
print(f"get_position, {self.player=}")
|
||||||
|
if not self.player:
|
||||||
|
return
|
||||||
return self.player.get_position()
|
return self.player.get_position()
|
||||||
|
|
||||||
def play(self, path):
|
def play(self, path):
|
||||||
@ -147,13 +148,13 @@ class Music:
|
|||||||
return self.fading > 0
|
return self.fading > 0
|
||||||
|
|
||||||
def set_position(self, ms):
|
def set_position(self, ms):
|
||||||
"Set current play time in milliseconds from start"
|
"""Set current play time in milliseconds from start"""
|
||||||
|
|
||||||
with lock:
|
with lock:
|
||||||
return self.player.set_time(ms)
|
return self.player.set_time(ms)
|
||||||
|
|
||||||
def set_volume(self, volume):
|
def set_volume(self, volume):
|
||||||
"Set maximum volume used for player"
|
"""Set maximum volume used for player"""
|
||||||
|
|
||||||
with lock:
|
with lock:
|
||||||
if not self.player:
|
if not self.player:
|
||||||
@ -163,7 +164,7 @@ class Music:
|
|||||||
self.player.audio_set_volume(volume)
|
self.player.audio_set_volume(volume)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"Immediately stop playing"
|
"""Immediately stop playing"""
|
||||||
|
|
||||||
DEBUG(f"music.stop(), {self.player=}", True)
|
DEBUG(f"music.stop(), {self.player=}", True)
|
||||||
|
|
||||||
|
|||||||
1006
app/musicmuster.py
1006
app/musicmuster.py
File diff suppressed because it is too large
Load Diff
1891
app/playlists.py
1891
app/playlists.py
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>1164</width>
|
<width>1280</width>
|
||||||
<height>857</height>
|
<height>857</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<string notr="true"/>
|
<string notr="true"/>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="centralwidget">
|
<widget class="QWidget" name="centralwidget">
|
||||||
<layout class="QGridLayout" name="gridLayout_3">
|
<layout class="QGridLayout" name="gridLayout_6">
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
<item>
|
<item>
|
||||||
@ -175,7 +175,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="ElideLabel" name="hdrNextTrack">
|
<widget class="QLabel" name="hdrNextTrack">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
@ -337,6 +337,9 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="btnAddFile">
|
<widget class="QPushButton" name="btnAddFile">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Add file</string>
|
<string>Add file</string>
|
||||||
</property>
|
</property>
|
||||||
@ -485,11 +488,11 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<property name="frameShadow">
|
<property name="frameShadow">
|
||||||
<enum>QFrame::Raised</enum>
|
<enum>QFrame::Raised</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QFormLayout" name="formLayout">
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label_2">
|
<widget class="QLabel" name="label_x">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Started at:</string>
|
<string>Track length:</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
@ -497,7 +500,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QLabel" name="label_start_tod">
|
<widget class="QLabel" name="label_track_length">
|
||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<family>FreeSans</family>
|
<family>FreeSans</family>
|
||||||
@ -505,7 +508,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>00:00:00</string>
|
<string>0:00</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="scaledContents">
|
<property name="scaledContents">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
@ -513,32 +516,6 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="label_3">
|
|
||||||
<property name="text">
|
|
||||||
<string>Silent at:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QLabel" name="label_silent_tod">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<family>FreeSans</family>
|
|
||||||
<pointsize>16</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>00:00:00</string>
|
|
||||||
</property>
|
|
||||||
<property name="scaledContents">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QLabel" name="label_7">
|
<widget class="QLabel" name="label_7">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Fade length:</string>
|
<string>Fade length:</string>
|
||||||
@ -548,7 +525,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="1" column="1">
|
||||||
<widget class="QLabel" name="label_fade_length">
|
<widget class="QLabel" name="label_fade_length">
|
||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
@ -564,6 +541,32 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>Silence length:</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QLabel" name="label_silence_length">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<family>FreeSans</family>
|
||||||
|
<pointsize>16</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>0:00</string>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -578,8 +581,8 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<property name="frameShadow">
|
<property name="frameShadow">
|
||||||
<enum>QFrame::Raised</enum>
|
<enum>QFrame::Raised</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item>
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Elapsed time</string>
|
<string>Elapsed time</string>
|
||||||
@ -589,7 +592,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="label_elapsed_timer">
|
<widget class="QLabel" name="label_elapsed_timer">
|
||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
@ -621,8 +624,8 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<property name="frameShadow">
|
<property name="frameShadow">
|
||||||
<enum>QFrame::Raised</enum>
|
<enum>QFrame::Raised</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
<layout class="QGridLayout" name="gridLayout_3">
|
||||||
<item>
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label_4">
|
<widget class="QLabel" name="label_4">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Fade</string>
|
<string>Fade</string>
|
||||||
@ -632,7 +635,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="label_fade_timer">
|
<widget class="QLabel" name="label_fade_timer">
|
||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
@ -664,8 +667,8 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<property name="frameShadow">
|
<property name="frameShadow">
|
||||||
<enum>QFrame::Raised</enum>
|
<enum>QFrame::Raised</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
<layout class="QGridLayout" name="gridLayout_4">
|
||||||
<item>
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label_5">
|
<widget class="QLabel" name="label_5">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Silent</string>
|
<string>Silent</string>
|
||||||
@ -675,7 +678,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="label_silent_timer">
|
<widget class="QLabel" name="label_silent_timer">
|
||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
@ -707,8 +710,8 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<property name="frameShadow">
|
<property name="frameShadow">
|
||||||
<enum>QFrame::Raised</enum>
|
<enum>QFrame::Raised</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
<layout class="QGridLayout" name="gridLayout_5">
|
||||||
<item>
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label_6">
|
<widget class="QLabel" name="label_6">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>End</string>
|
<string>End</string>
|
||||||
@ -718,7 +721,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="label_end_timer">
|
<widget class="QLabel" name="label_end_timer">
|
||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
@ -748,15 +751,18 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>1164</width>
|
<width>1280</width>
|
||||||
<height>29</height>
|
<height>24</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuFile">
|
<widget class="QMenu" name="menuFile">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Fi&le</string>
|
<string>Fi&le</string>
|
||||||
</property>
|
</property>
|
||||||
|
<addaction name="actionImport"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
<addaction name="actionE_xit"/>
|
<addaction name="actionE_xit"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menuPlaylist">
|
<widget class="QMenu" name="menuPlaylist">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
@ -769,22 +775,22 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<addaction name="actionDeletePlaylist"/>
|
<addaction name="actionDeletePlaylist"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionSearch_database"/>
|
<addaction name="actionSearch_database"/>
|
||||||
<addaction name="actionAdd_file"/>
|
|
||||||
<addaction name="actionAdd_note"/>
|
<addaction name="actionAdd_note"/>
|
||||||
<addaction name="action_Clear_selection"/>
|
<addaction name="action_Clear_selection"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionSelect_previous_track"/>
|
||||||
|
<addaction name="actionSelect_next_track"/>
|
||||||
|
<addaction name="actionSetNext"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
<addaction name="actionSelect_unplayed_tracks"/>
|
<addaction name="actionSelect_unplayed_tracks"/>
|
||||||
<addaction name="actionSelect_played_tracks"/>
|
<addaction name="actionSelect_played_tracks"/>
|
||||||
<addaction name="actionMoveSelected"/>
|
<addaction name="actionMoveSelected"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionExport_playlist"/>
|
<addaction name="actionExport_playlist"/>
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionSelect_next_track"/>
|
|
||||||
<addaction name="actionSelect_previous_track"/>
|
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menu_Tracks">
|
<widget class="QMenu" name="menu_Music">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>&Tracks</string>
|
<string>&Music</string>
|
||||||
</property>
|
</property>
|
||||||
<addaction name="actionPlay_next"/>
|
<addaction name="actionPlay_next"/>
|
||||||
<addaction name="actionSkip_next"/>
|
<addaction name="actionSkip_next"/>
|
||||||
@ -792,21 +798,11 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<addaction name="actionStop"/>
|
<addaction name="actionStop"/>
|
||||||
<addaction name="action_Resume_previous"/>
|
<addaction name="action_Resume_previous"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionSetNext"/>
|
<addaction name="actionEnable_controls"/>
|
||||||
</widget>
|
|
||||||
<widget class="QMenu" name="menuTest">
|
|
||||||
<property name="title">
|
|
||||||
<string>TestMo&de</string>
|
|
||||||
</property>
|
|
||||||
<addaction name="actionTestFunction"/>
|
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionSkipToFade"/>
|
|
||||||
<addaction name="actionSkipToEnd"/>
|
|
||||||
</widget>
|
</widget>
|
||||||
<addaction name="menuFile"/>
|
<addaction name="menuFile"/>
|
||||||
<addaction name="menuPlaylist"/>
|
<addaction name="menuPlaylist"/>
|
||||||
<addaction name="menu_Tracks"/>
|
<addaction name="menu_Music"/>
|
||||||
<addaction name="menuTest"/>
|
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QStatusBar" name="statusbar">
|
<widget class="QStatusBar" name="statusbar">
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
@ -1013,14 +1009,20 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<string>Ctrl+T</string>
|
<string>Ctrl+T</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionEnable_controls">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable controls</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionImport">
|
||||||
|
<property name="text">
|
||||||
|
<string>Import...</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+Shift+I</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
|
||||||
<customwidget>
|
|
||||||
<class>ElideLabel</class>
|
|
||||||
<extends>QLabel</extends>
|
|
||||||
<header>musicmuster</header>
|
|
||||||
</customwidget>
|
|
||||||
</customwidgets>
|
|
||||||
<resources>
|
<resources>
|
||||||
<include location="icons.qrc"/>
|
<include location="icons.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Form implementation generated from reading ui file 'ui/main_window.ui'
|
# Form implementation generated from reading ui file 'ui/main_window.ui'
|
||||||
#
|
#
|
||||||
# Created by: PyQt5 UI code generator 5.15.4
|
# Created by: PyQt5 UI code generator 5.15.6
|
||||||
#
|
#
|
||||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||||
# run again. Do not edit this file unless you know what you are doing.
|
# run again. Do not edit this file unless you know what you are doing.
|
||||||
@ -14,12 +14,12 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
|||||||
class Ui_MainWindow(object):
|
class Ui_MainWindow(object):
|
||||||
def setupUi(self, MainWindow):
|
def setupUi(self, MainWindow):
|
||||||
MainWindow.setObjectName("MainWindow")
|
MainWindow.setObjectName("MainWindow")
|
||||||
MainWindow.resize(1164, 857)
|
MainWindow.resize(1280, 857)
|
||||||
MainWindow.setStyleSheet("")
|
MainWindow.setStyleSheet("")
|
||||||
self.centralwidget = QtWidgets.QWidget(MainWindow)
|
self.centralwidget = QtWidgets.QWidget(MainWindow)
|
||||||
self.centralwidget.setObjectName("centralwidget")
|
self.centralwidget.setObjectName("centralwidget")
|
||||||
self.gridLayout_3 = QtWidgets.QGridLayout(self.centralwidget)
|
self.gridLayout_6 = QtWidgets.QGridLayout(self.centralwidget)
|
||||||
self.gridLayout_3.setObjectName("gridLayout_3")
|
self.gridLayout_6.setObjectName("gridLayout_6")
|
||||||
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
|
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
|
||||||
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
||||||
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
|
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
|
||||||
@ -99,7 +99,7 @@ class Ui_MainWindow(object):
|
|||||||
self.hdrCurrentTrack.setWordWrap(True)
|
self.hdrCurrentTrack.setWordWrap(True)
|
||||||
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
|
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
|
||||||
self.verticalLayout.addWidget(self.hdrCurrentTrack)
|
self.verticalLayout.addWidget(self.hdrCurrentTrack)
|
||||||
self.hdrNextTrack = ElideLabel(self.centralwidget)
|
self.hdrNextTrack = QtWidgets.QLabel(self.centralwidget)
|
||||||
self.hdrNextTrack.setMinimumSize(QtCore.QSize(0, 39))
|
self.hdrNextTrack.setMinimumSize(QtCore.QSize(0, 39))
|
||||||
self.hdrNextTrack.setMaximumSize(QtCore.QSize(16777215, 39))
|
self.hdrNextTrack.setMaximumSize(QtCore.QSize(16777215, 39))
|
||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
@ -129,7 +129,7 @@ class Ui_MainWindow(object):
|
|||||||
self.lblTOD.setObjectName("lblTOD")
|
self.lblTOD.setObjectName("lblTOD")
|
||||||
self.gridLayout_2.addWidget(self.lblTOD, 0, 0, 1, 1)
|
self.gridLayout_2.addWidget(self.lblTOD, 0, 0, 1, 1)
|
||||||
self.horizontalLayout_3.addWidget(self.frame_2)
|
self.horizontalLayout_3.addWidget(self.frame_2)
|
||||||
self.gridLayout_3.addLayout(self.horizontalLayout_3, 0, 0, 1, 1)
|
self.gridLayout_6.addLayout(self.horizontalLayout_3, 0, 0, 1, 1)
|
||||||
self.frame_5 = QtWidgets.QFrame(self.centralwidget)
|
self.frame_5 = QtWidgets.QFrame(self.centralwidget)
|
||||||
self.frame_5.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
self.frame_5.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
self.frame_5.setFrameShadow(QtWidgets.QFrame.Raised)
|
self.frame_5.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
@ -162,6 +162,7 @@ class Ui_MainWindow(object):
|
|||||||
self.btnDatabase.setObjectName("btnDatabase")
|
self.btnDatabase.setObjectName("btnDatabase")
|
||||||
self.horizontalLayout.addWidget(self.btnDatabase)
|
self.horizontalLayout.addWidget(self.btnDatabase)
|
||||||
self.btnAddFile = QtWidgets.QPushButton(self.frame_5)
|
self.btnAddFile = QtWidgets.QPushButton(self.frame_5)
|
||||||
|
self.btnAddFile.setEnabled(False)
|
||||||
icon3 = QtGui.QIcon()
|
icon3 = QtGui.QIcon()
|
||||||
icon3.addPixmap(QtGui.QPixmap(":/icons/open_file"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
|
icon3.addPixmap(QtGui.QPixmap(":/icons/open_file"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
|
||||||
self.btnAddFile.setIcon(icon3)
|
self.btnAddFile.setIcon(icon3)
|
||||||
@ -205,49 +206,37 @@ class Ui_MainWindow(object):
|
|||||||
self.spnVolume.setProperty("value", 100)
|
self.spnVolume.setProperty("value", 100)
|
||||||
self.spnVolume.setObjectName("spnVolume")
|
self.spnVolume.setObjectName("spnVolume")
|
||||||
self.horizontalLayout.addWidget(self.spnVolume)
|
self.horizontalLayout.addWidget(self.spnVolume)
|
||||||
self.gridLayout_3.addWidget(self.frame_5, 1, 0, 1, 1)
|
self.gridLayout_6.addWidget(self.frame_5, 1, 0, 1, 1)
|
||||||
self.tabPlaylist = QtWidgets.QTabWidget(self.centralwidget)
|
self.tabPlaylist = QtWidgets.QTabWidget(self.centralwidget)
|
||||||
self.tabPlaylist.setDocumentMode(False)
|
self.tabPlaylist.setDocumentMode(False)
|
||||||
self.tabPlaylist.setTabsClosable(True)
|
self.tabPlaylist.setTabsClosable(True)
|
||||||
self.tabPlaylist.setMovable(True)
|
self.tabPlaylist.setMovable(True)
|
||||||
self.tabPlaylist.setObjectName("tabPlaylist")
|
self.tabPlaylist.setObjectName("tabPlaylist")
|
||||||
self.gridLayout_3.addWidget(self.tabPlaylist, 2, 0, 1, 1)
|
self.gridLayout_6.addWidget(self.tabPlaylist, 2, 0, 1, 1)
|
||||||
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||||
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||||
self.frame = QtWidgets.QFrame(self.centralwidget)
|
self.frame = QtWidgets.QFrame(self.centralwidget)
|
||||||
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
|
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
self.frame.setObjectName("frame")
|
self.frame.setObjectName("frame")
|
||||||
self.gridLayout = QtWidgets.QGridLayout(self.frame)
|
self.formLayout = QtWidgets.QFormLayout(self.frame)
|
||||||
self.gridLayout.setObjectName("gridLayout")
|
self.formLayout.setObjectName("formLayout")
|
||||||
self.label_2 = QtWidgets.QLabel(self.frame)
|
self.label_x = QtWidgets.QLabel(self.frame)
|
||||||
self.label_2.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
self.label_x.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
||||||
self.label_2.setObjectName("label_2")
|
self.label_x.setObjectName("label_x")
|
||||||
self.gridLayout.addWidget(self.label_2, 0, 0, 1, 1)
|
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_x)
|
||||||
self.label_start_tod = QtWidgets.QLabel(self.frame)
|
self.label_track_length = QtWidgets.QLabel(self.frame)
|
||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
font.setFamily("FreeSans")
|
font.setFamily("FreeSans")
|
||||||
font.setPointSize(16)
|
font.setPointSize(16)
|
||||||
self.label_start_tod.setFont(font)
|
self.label_track_length.setFont(font)
|
||||||
self.label_start_tod.setScaledContents(False)
|
self.label_track_length.setScaledContents(False)
|
||||||
self.label_start_tod.setObjectName("label_start_tod")
|
self.label_track_length.setObjectName("label_track_length")
|
||||||
self.gridLayout.addWidget(self.label_start_tod, 0, 1, 1, 1)
|
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.label_track_length)
|
||||||
self.label_3 = QtWidgets.QLabel(self.frame)
|
|
||||||
self.label_3.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
|
||||||
self.label_3.setObjectName("label_3")
|
|
||||||
self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1)
|
|
||||||
self.label_silent_tod = QtWidgets.QLabel(self.frame)
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setFamily("FreeSans")
|
|
||||||
font.setPointSize(16)
|
|
||||||
self.label_silent_tod.setFont(font)
|
|
||||||
self.label_silent_tod.setScaledContents(False)
|
|
||||||
self.label_silent_tod.setObjectName("label_silent_tod")
|
|
||||||
self.gridLayout.addWidget(self.label_silent_tod, 1, 1, 1, 1)
|
|
||||||
self.label_7 = QtWidgets.QLabel(self.frame)
|
self.label_7 = QtWidgets.QLabel(self.frame)
|
||||||
self.label_7.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
self.label_7.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
||||||
self.label_7.setObjectName("label_7")
|
self.label_7.setObjectName("label_7")
|
||||||
self.gridLayout.addWidget(self.label_7, 2, 0, 1, 1)
|
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_7)
|
||||||
self.label_fade_length = QtWidgets.QLabel(self.frame)
|
self.label_fade_length = QtWidgets.QLabel(self.frame)
|
||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
font.setFamily("FreeSans")
|
font.setFamily("FreeSans")
|
||||||
@ -255,19 +244,31 @@ class Ui_MainWindow(object):
|
|||||||
self.label_fade_length.setFont(font)
|
self.label_fade_length.setFont(font)
|
||||||
self.label_fade_length.setScaledContents(False)
|
self.label_fade_length.setScaledContents(False)
|
||||||
self.label_fade_length.setObjectName("label_fade_length")
|
self.label_fade_length.setObjectName("label_fade_length")
|
||||||
self.gridLayout.addWidget(self.label_fade_length, 2, 1, 1, 1)
|
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.label_fade_length)
|
||||||
|
self.label_3 = QtWidgets.QLabel(self.frame)
|
||||||
|
self.label_3.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
||||||
|
self.label_3.setObjectName("label_3")
|
||||||
|
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_3)
|
||||||
|
self.label_silence_length = QtWidgets.QLabel(self.frame)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setFamily("FreeSans")
|
||||||
|
font.setPointSize(16)
|
||||||
|
self.label_silence_length.setFont(font)
|
||||||
|
self.label_silence_length.setScaledContents(False)
|
||||||
|
self.label_silence_length.setObjectName("label_silence_length")
|
||||||
|
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.label_silence_length)
|
||||||
self.horizontalLayout_2.addWidget(self.frame)
|
self.horizontalLayout_2.addWidget(self.frame)
|
||||||
self.frame_elapsed = QtWidgets.QFrame(self.centralwidget)
|
self.frame_elapsed = QtWidgets.QFrame(self.centralwidget)
|
||||||
self.frame_elapsed.setStyleSheet("")
|
self.frame_elapsed.setStyleSheet("")
|
||||||
self.frame_elapsed.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
self.frame_elapsed.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
self.frame_elapsed.setFrameShadow(QtWidgets.QFrame.Raised)
|
self.frame_elapsed.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
self.frame_elapsed.setObjectName("frame_elapsed")
|
self.frame_elapsed.setObjectName("frame_elapsed")
|
||||||
self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.frame_elapsed)
|
self.gridLayout = QtWidgets.QGridLayout(self.frame_elapsed)
|
||||||
self.verticalLayout_5.setObjectName("verticalLayout_5")
|
self.gridLayout.setObjectName("gridLayout")
|
||||||
self.label = QtWidgets.QLabel(self.frame_elapsed)
|
self.label = QtWidgets.QLabel(self.frame_elapsed)
|
||||||
self.label.setAlignment(QtCore.Qt.AlignCenter)
|
self.label.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
self.label.setObjectName("label")
|
self.label.setObjectName("label")
|
||||||
self.verticalLayout_5.addWidget(self.label)
|
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
|
||||||
self.label_elapsed_timer = QtWidgets.QLabel(self.frame_elapsed)
|
self.label_elapsed_timer = QtWidgets.QLabel(self.frame_elapsed)
|
||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
font.setFamily("FreeSans")
|
font.setFamily("FreeSans")
|
||||||
@ -277,19 +278,19 @@ class Ui_MainWindow(object):
|
|||||||
self.label_elapsed_timer.setFont(font)
|
self.label_elapsed_timer.setFont(font)
|
||||||
self.label_elapsed_timer.setAlignment(QtCore.Qt.AlignCenter)
|
self.label_elapsed_timer.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
self.label_elapsed_timer.setObjectName("label_elapsed_timer")
|
self.label_elapsed_timer.setObjectName("label_elapsed_timer")
|
||||||
self.verticalLayout_5.addWidget(self.label_elapsed_timer)
|
self.gridLayout.addWidget(self.label_elapsed_timer, 1, 0, 1, 1)
|
||||||
self.horizontalLayout_2.addWidget(self.frame_elapsed)
|
self.horizontalLayout_2.addWidget(self.frame_elapsed)
|
||||||
self.frame_fade = QtWidgets.QFrame(self.centralwidget)
|
self.frame_fade = QtWidgets.QFrame(self.centralwidget)
|
||||||
self.frame_fade.setStyleSheet("")
|
self.frame_fade.setStyleSheet("")
|
||||||
self.frame_fade.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
self.frame_fade.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
self.frame_fade.setFrameShadow(QtWidgets.QFrame.Raised)
|
self.frame_fade.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
self.frame_fade.setObjectName("frame_fade")
|
self.frame_fade.setObjectName("frame_fade")
|
||||||
self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.frame_fade)
|
self.gridLayout_3 = QtWidgets.QGridLayout(self.frame_fade)
|
||||||
self.verticalLayout_6.setObjectName("verticalLayout_6")
|
self.gridLayout_3.setObjectName("gridLayout_3")
|
||||||
self.label_4 = QtWidgets.QLabel(self.frame_fade)
|
self.label_4 = QtWidgets.QLabel(self.frame_fade)
|
||||||
self.label_4.setAlignment(QtCore.Qt.AlignCenter)
|
self.label_4.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
self.label_4.setObjectName("label_4")
|
self.label_4.setObjectName("label_4")
|
||||||
self.verticalLayout_6.addWidget(self.label_4)
|
self.gridLayout_3.addWidget(self.label_4, 0, 0, 1, 1)
|
||||||
self.label_fade_timer = QtWidgets.QLabel(self.frame_fade)
|
self.label_fade_timer = QtWidgets.QLabel(self.frame_fade)
|
||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
font.setFamily("FreeSans")
|
font.setFamily("FreeSans")
|
||||||
@ -299,19 +300,19 @@ class Ui_MainWindow(object):
|
|||||||
self.label_fade_timer.setFont(font)
|
self.label_fade_timer.setFont(font)
|
||||||
self.label_fade_timer.setAlignment(QtCore.Qt.AlignCenter)
|
self.label_fade_timer.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
self.label_fade_timer.setObjectName("label_fade_timer")
|
self.label_fade_timer.setObjectName("label_fade_timer")
|
||||||
self.verticalLayout_6.addWidget(self.label_fade_timer)
|
self.gridLayout_3.addWidget(self.label_fade_timer, 1, 0, 1, 1)
|
||||||
self.horizontalLayout_2.addWidget(self.frame_fade)
|
self.horizontalLayout_2.addWidget(self.frame_fade)
|
||||||
self.frame_silent = QtWidgets.QFrame(self.centralwidget)
|
self.frame_silent = QtWidgets.QFrame(self.centralwidget)
|
||||||
self.frame_silent.setStyleSheet("")
|
self.frame_silent.setStyleSheet("")
|
||||||
self.frame_silent.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
self.frame_silent.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
self.frame_silent.setFrameShadow(QtWidgets.QFrame.Raised)
|
self.frame_silent.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
self.frame_silent.setObjectName("frame_silent")
|
self.frame_silent.setObjectName("frame_silent")
|
||||||
self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.frame_silent)
|
self.gridLayout_4 = QtWidgets.QGridLayout(self.frame_silent)
|
||||||
self.verticalLayout_7.setObjectName("verticalLayout_7")
|
self.gridLayout_4.setObjectName("gridLayout_4")
|
||||||
self.label_5 = QtWidgets.QLabel(self.frame_silent)
|
self.label_5 = QtWidgets.QLabel(self.frame_silent)
|
||||||
self.label_5.setAlignment(QtCore.Qt.AlignCenter)
|
self.label_5.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
self.label_5.setObjectName("label_5")
|
self.label_5.setObjectName("label_5")
|
||||||
self.verticalLayout_7.addWidget(self.label_5)
|
self.gridLayout_4.addWidget(self.label_5, 0, 0, 1, 1)
|
||||||
self.label_silent_timer = QtWidgets.QLabel(self.frame_silent)
|
self.label_silent_timer = QtWidgets.QLabel(self.frame_silent)
|
||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
font.setFamily("FreeSans")
|
font.setFamily("FreeSans")
|
||||||
@ -321,19 +322,19 @@ class Ui_MainWindow(object):
|
|||||||
self.label_silent_timer.setFont(font)
|
self.label_silent_timer.setFont(font)
|
||||||
self.label_silent_timer.setAlignment(QtCore.Qt.AlignCenter)
|
self.label_silent_timer.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
self.label_silent_timer.setObjectName("label_silent_timer")
|
self.label_silent_timer.setObjectName("label_silent_timer")
|
||||||
self.verticalLayout_7.addWidget(self.label_silent_timer)
|
self.gridLayout_4.addWidget(self.label_silent_timer, 1, 0, 1, 1)
|
||||||
self.horizontalLayout_2.addWidget(self.frame_silent)
|
self.horizontalLayout_2.addWidget(self.frame_silent)
|
||||||
self.frame_end = QtWidgets.QFrame(self.centralwidget)
|
self.frame_end = QtWidgets.QFrame(self.centralwidget)
|
||||||
self.frame_end.setStyleSheet("")
|
self.frame_end.setStyleSheet("")
|
||||||
self.frame_end.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
self.frame_end.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
self.frame_end.setFrameShadow(QtWidgets.QFrame.Raised)
|
self.frame_end.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
self.frame_end.setObjectName("frame_end")
|
self.frame_end.setObjectName("frame_end")
|
||||||
self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.frame_end)
|
self.gridLayout_5 = QtWidgets.QGridLayout(self.frame_end)
|
||||||
self.verticalLayout_8.setObjectName("verticalLayout_8")
|
self.gridLayout_5.setObjectName("gridLayout_5")
|
||||||
self.label_6 = QtWidgets.QLabel(self.frame_end)
|
self.label_6 = QtWidgets.QLabel(self.frame_end)
|
||||||
self.label_6.setAlignment(QtCore.Qt.AlignCenter)
|
self.label_6.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
self.label_6.setObjectName("label_6")
|
self.label_6.setObjectName("label_6")
|
||||||
self.verticalLayout_8.addWidget(self.label_6)
|
self.gridLayout_5.addWidget(self.label_6, 0, 0, 1, 1)
|
||||||
self.label_end_timer = QtWidgets.QLabel(self.frame_end)
|
self.label_end_timer = QtWidgets.QLabel(self.frame_end)
|
||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
font.setFamily("FreeSans")
|
font.setFamily("FreeSans")
|
||||||
@ -343,21 +344,19 @@ class Ui_MainWindow(object):
|
|||||||
self.label_end_timer.setFont(font)
|
self.label_end_timer.setFont(font)
|
||||||
self.label_end_timer.setAlignment(QtCore.Qt.AlignCenter)
|
self.label_end_timer.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
self.label_end_timer.setObjectName("label_end_timer")
|
self.label_end_timer.setObjectName("label_end_timer")
|
||||||
self.verticalLayout_8.addWidget(self.label_end_timer)
|
self.gridLayout_5.addWidget(self.label_end_timer, 1, 0, 1, 1)
|
||||||
self.horizontalLayout_2.addWidget(self.frame_end)
|
self.horizontalLayout_2.addWidget(self.frame_end)
|
||||||
self.gridLayout_3.addLayout(self.horizontalLayout_2, 3, 0, 1, 1)
|
self.gridLayout_6.addLayout(self.horizontalLayout_2, 3, 0, 1, 1)
|
||||||
MainWindow.setCentralWidget(self.centralwidget)
|
MainWindow.setCentralWidget(self.centralwidget)
|
||||||
self.menubar = QtWidgets.QMenuBar(MainWindow)
|
self.menubar = QtWidgets.QMenuBar(MainWindow)
|
||||||
self.menubar.setGeometry(QtCore.QRect(0, 0, 1164, 29))
|
self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 24))
|
||||||
self.menubar.setObjectName("menubar")
|
self.menubar.setObjectName("menubar")
|
||||||
self.menuFile = QtWidgets.QMenu(self.menubar)
|
self.menuFile = QtWidgets.QMenu(self.menubar)
|
||||||
self.menuFile.setObjectName("menuFile")
|
self.menuFile.setObjectName("menuFile")
|
||||||
self.menuPlaylist = QtWidgets.QMenu(self.menubar)
|
self.menuPlaylist = QtWidgets.QMenu(self.menubar)
|
||||||
self.menuPlaylist.setObjectName("menuPlaylist")
|
self.menuPlaylist.setObjectName("menuPlaylist")
|
||||||
self.menu_Tracks = QtWidgets.QMenu(self.menubar)
|
self.menu_Music = QtWidgets.QMenu(self.menubar)
|
||||||
self.menu_Tracks.setObjectName("menu_Tracks")
|
self.menu_Music.setObjectName("menu_Music")
|
||||||
self.menuTest = QtWidgets.QMenu(self.menubar)
|
|
||||||
self.menuTest.setObjectName("menuTest")
|
|
||||||
MainWindow.setMenuBar(self.menubar)
|
MainWindow.setMenuBar(self.menubar)
|
||||||
self.statusbar = QtWidgets.QStatusBar(MainWindow)
|
self.statusbar = QtWidgets.QStatusBar(MainWindow)
|
||||||
self.statusbar.setEnabled(True)
|
self.statusbar.setEnabled(True)
|
||||||
@ -440,7 +439,14 @@ class Ui_MainWindow(object):
|
|||||||
self.actionSelect_unplayed_tracks.setObjectName("actionSelect_unplayed_tracks")
|
self.actionSelect_unplayed_tracks.setObjectName("actionSelect_unplayed_tracks")
|
||||||
self.actionAdd_note = QtWidgets.QAction(MainWindow)
|
self.actionAdd_note = QtWidgets.QAction(MainWindow)
|
||||||
self.actionAdd_note.setObjectName("actionAdd_note")
|
self.actionAdd_note.setObjectName("actionAdd_note")
|
||||||
|
self.actionEnable_controls = QtWidgets.QAction(MainWindow)
|
||||||
|
self.actionEnable_controls.setObjectName("actionEnable_controls")
|
||||||
|
self.actionImport = QtWidgets.QAction(MainWindow)
|
||||||
|
self.actionImport.setObjectName("actionImport")
|
||||||
|
self.menuFile.addAction(self.actionImport)
|
||||||
|
self.menuFile.addSeparator()
|
||||||
self.menuFile.addAction(self.actionE_xit)
|
self.menuFile.addAction(self.actionE_xit)
|
||||||
|
self.menuFile.addSeparator()
|
||||||
self.menuPlaylist.addAction(self.actionNewPlaylist)
|
self.menuPlaylist.addAction(self.actionNewPlaylist)
|
||||||
self.menuPlaylist.addAction(self.actionOpenPlaylist)
|
self.menuPlaylist.addAction(self.actionOpenPlaylist)
|
||||||
self.menuPlaylist.addAction(self.actionClosePlaylist)
|
self.menuPlaylist.addAction(self.actionClosePlaylist)
|
||||||
@ -448,37 +454,32 @@ class Ui_MainWindow(object):
|
|||||||
self.menuPlaylist.addAction(self.actionDeletePlaylist)
|
self.menuPlaylist.addAction(self.actionDeletePlaylist)
|
||||||
self.menuPlaylist.addSeparator()
|
self.menuPlaylist.addSeparator()
|
||||||
self.menuPlaylist.addAction(self.actionSearch_database)
|
self.menuPlaylist.addAction(self.actionSearch_database)
|
||||||
self.menuPlaylist.addAction(self.actionAdd_file)
|
|
||||||
self.menuPlaylist.addAction(self.actionAdd_note)
|
self.menuPlaylist.addAction(self.actionAdd_note)
|
||||||
self.menuPlaylist.addAction(self.action_Clear_selection)
|
self.menuPlaylist.addAction(self.action_Clear_selection)
|
||||||
self.menuPlaylist.addSeparator()
|
self.menuPlaylist.addSeparator()
|
||||||
|
self.menuPlaylist.addAction(self.actionSelect_previous_track)
|
||||||
|
self.menuPlaylist.addAction(self.actionSelect_next_track)
|
||||||
|
self.menuPlaylist.addAction(self.actionSetNext)
|
||||||
|
self.menuPlaylist.addSeparator()
|
||||||
self.menuPlaylist.addAction(self.actionSelect_unplayed_tracks)
|
self.menuPlaylist.addAction(self.actionSelect_unplayed_tracks)
|
||||||
self.menuPlaylist.addAction(self.actionSelect_played_tracks)
|
self.menuPlaylist.addAction(self.actionSelect_played_tracks)
|
||||||
self.menuPlaylist.addAction(self.actionMoveSelected)
|
self.menuPlaylist.addAction(self.actionMoveSelected)
|
||||||
self.menuPlaylist.addSeparator()
|
self.menuPlaylist.addSeparator()
|
||||||
self.menuPlaylist.addAction(self.actionExport_playlist)
|
self.menuPlaylist.addAction(self.actionExport_playlist)
|
||||||
self.menuPlaylist.addSeparator()
|
self.menu_Music.addAction(self.actionPlay_next)
|
||||||
self.menuPlaylist.addAction(self.actionSelect_next_track)
|
self.menu_Music.addAction(self.actionSkip_next)
|
||||||
self.menuPlaylist.addAction(self.actionSelect_previous_track)
|
self.menu_Music.addAction(self.actionFade)
|
||||||
self.menu_Tracks.addAction(self.actionPlay_next)
|
self.menu_Music.addAction(self.actionStop)
|
||||||
self.menu_Tracks.addAction(self.actionSkip_next)
|
self.menu_Music.addAction(self.action_Resume_previous)
|
||||||
self.menu_Tracks.addAction(self.actionFade)
|
self.menu_Music.addSeparator()
|
||||||
self.menu_Tracks.addAction(self.actionStop)
|
self.menu_Music.addAction(self.actionEnable_controls)
|
||||||
self.menu_Tracks.addAction(self.action_Resume_previous)
|
|
||||||
self.menu_Tracks.addSeparator()
|
|
||||||
self.menu_Tracks.addAction(self.actionSetNext)
|
|
||||||
self.menuTest.addAction(self.actionTestFunction)
|
|
||||||
self.menuTest.addSeparator()
|
|
||||||
self.menuTest.addAction(self.actionSkipToFade)
|
|
||||||
self.menuTest.addAction(self.actionSkipToEnd)
|
|
||||||
self.menubar.addAction(self.menuFile.menuAction())
|
self.menubar.addAction(self.menuFile.menuAction())
|
||||||
self.menubar.addAction(self.menuPlaylist.menuAction())
|
self.menubar.addAction(self.menuPlaylist.menuAction())
|
||||||
self.menubar.addAction(self.menu_Tracks.menuAction())
|
self.menubar.addAction(self.menu_Music.menuAction())
|
||||||
self.menubar.addAction(self.menuTest.menuAction())
|
|
||||||
|
|
||||||
self.retranslateUi(MainWindow)
|
self.retranslateUi(MainWindow)
|
||||||
self.tabPlaylist.setCurrentIndex(-1)
|
self.tabPlaylist.setCurrentIndex(-1)
|
||||||
self.actionE_xit.triggered.connect(MainWindow.close)
|
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
|
||||||
QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||||
|
|
||||||
def retranslateUi(self, MainWindow):
|
def retranslateUi(self, MainWindow):
|
||||||
@ -496,12 +497,12 @@ class Ui_MainWindow(object):
|
|||||||
self.btnSetNext.setText(_translate("MainWindow", "Set next"))
|
self.btnSetNext.setText(_translate("MainWindow", "Set next"))
|
||||||
self.btnStop.setText(_translate("MainWindow", "Stop"))
|
self.btnStop.setText(_translate("MainWindow", "Stop"))
|
||||||
self.btnFade.setText(_translate("MainWindow", "Fade"))
|
self.btnFade.setText(_translate("MainWindow", "Fade"))
|
||||||
self.label_2.setText(_translate("MainWindow", "Started at:"))
|
self.label_x.setText(_translate("MainWindow", "Track length:"))
|
||||||
self.label_start_tod.setText(_translate("MainWindow", "00:00:00"))
|
self.label_track_length.setText(_translate("MainWindow", "0:00"))
|
||||||
self.label_3.setText(_translate("MainWindow", "Silent at:"))
|
|
||||||
self.label_silent_tod.setText(_translate("MainWindow", "00:00:00"))
|
|
||||||
self.label_7.setText(_translate("MainWindow", "Fade length:"))
|
self.label_7.setText(_translate("MainWindow", "Fade length:"))
|
||||||
self.label_fade_length.setText(_translate("MainWindow", "0:00"))
|
self.label_fade_length.setText(_translate("MainWindow", "0:00"))
|
||||||
|
self.label_3.setText(_translate("MainWindow", "Silence length:"))
|
||||||
|
self.label_silence_length.setText(_translate("MainWindow", "0:00"))
|
||||||
self.label.setText(_translate("MainWindow", "Elapsed time"))
|
self.label.setText(_translate("MainWindow", "Elapsed time"))
|
||||||
self.label_elapsed_timer.setText(_translate("MainWindow", "00:00"))
|
self.label_elapsed_timer.setText(_translate("MainWindow", "00:00"))
|
||||||
self.label_4.setText(_translate("MainWindow", "Fade"))
|
self.label_4.setText(_translate("MainWindow", "Fade"))
|
||||||
@ -512,8 +513,7 @@ class Ui_MainWindow(object):
|
|||||||
self.label_end_timer.setText(_translate("MainWindow", "00:00"))
|
self.label_end_timer.setText(_translate("MainWindow", "00:00"))
|
||||||
self.menuFile.setTitle(_translate("MainWindow", "Fi&le"))
|
self.menuFile.setTitle(_translate("MainWindow", "Fi&le"))
|
||||||
self.menuPlaylist.setTitle(_translate("MainWindow", "Pla&ylist"))
|
self.menuPlaylist.setTitle(_translate("MainWindow", "Pla&ylist"))
|
||||||
self.menu_Tracks.setTitle(_translate("MainWindow", "&Tracks"))
|
self.menu_Music.setTitle(_translate("MainWindow", "&Music"))
|
||||||
self.menuTest.setTitle(_translate("MainWindow", "TestMo&de"))
|
|
||||||
self.actionPlay_next.setText(_translate("MainWindow", "&Play next"))
|
self.actionPlay_next.setText(_translate("MainWindow", "&Play next"))
|
||||||
self.actionPlay_next.setShortcut(_translate("MainWindow", "Return"))
|
self.actionPlay_next.setShortcut(_translate("MainWindow", "Return"))
|
||||||
self.actionSkip_next.setText(_translate("MainWindow", "Skip to &next"))
|
self.actionSkip_next.setText(_translate("MainWindow", "Skip to &next"))
|
||||||
@ -550,5 +550,7 @@ class Ui_MainWindow(object):
|
|||||||
self.actionSelect_unplayed_tracks.setText(_translate("MainWindow", "Select unplayed tracks"))
|
self.actionSelect_unplayed_tracks.setText(_translate("MainWindow", "Select unplayed tracks"))
|
||||||
self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
|
self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
|
||||||
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
|
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
|
||||||
from musicmuster import ElideLabel
|
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))
|
||||||
|
self.actionImport.setText(_translate("MainWindow", "Import..."))
|
||||||
|
self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I"))
|
||||||
import icons_rc
|
import icons_rc
|
||||||
|
|||||||
17
app/ui_helpers.py
Normal file
17
app/ui_helpers.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QFontMetrics, QPainter
|
||||||
|
from PyQt5.QtWidgets import QLabel
|
||||||
|
|
||||||
|
|
||||||
|
class ElideLabel(QLabel):
|
||||||
|
"""
|
||||||
|
From https://stackoverflow.com/questions/11446478/
|
||||||
|
pyside-pyqt-truncate-text-in-qlabel-based-on-minimumsize
|
||||||
|
"""
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
painter = QPainter(self)
|
||||||
|
metrics = QFontMetrics(self.font())
|
||||||
|
elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())
|
||||||
|
|
||||||
|
painter.drawText(self.rect(), self.alignment(), elided)
|
||||||
@ -6,27 +6,31 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from helpers import show_warning
|
from helpers import (
|
||||||
|
fade_point,
|
||||||
|
get_audio_segment,
|
||||||
|
get_tags,
|
||||||
|
leading_silence,
|
||||||
|
trailing_silence,
|
||||||
|
)
|
||||||
from log import DEBUG, INFO
|
from log import DEBUG, INFO
|
||||||
from model import Notes, Playdates, PlaylistTracks, Session, Tracks
|
from models import Notes, Playdates, Session, Tracks
|
||||||
from mutagen.flac import FLAC
|
from mutagen.flac import FLAC
|
||||||
from mutagen.mp3 import MP3
|
from mutagen.mp3 import MP3
|
||||||
from pydub import AudioSegment, effects
|
from pydub import effects
|
||||||
from tinytag import TinyTag
|
|
||||||
|
|
||||||
# Globals (I know)
|
# Globals (I know)
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"Main loop"
|
"""Main loop"""
|
||||||
|
|
||||||
DEBUG("Starting")
|
DEBUG("Starting")
|
||||||
|
|
||||||
# Parse command line
|
|
||||||
p = argparse.ArgumentParser()
|
p = argparse.ArgumentParser()
|
||||||
# Only allow one option to be specified
|
# Only allow one option to be specified
|
||||||
group = p.add_mutually_exclusive_group()
|
group = p.add_mutually_exclusive_group()
|
||||||
|
|
||||||
group.add_argument('-u', '--update',
|
group.add_argument('-u', '--update',
|
||||||
action="store_true", dest="update",
|
action="store_true", dest="update",
|
||||||
default=False, help="Update database")
|
default=False, help="Update database")
|
||||||
@ -56,7 +60,7 @@ def main():
|
|||||||
DEBUG("Finished")
|
DEBUG("Finished")
|
||||||
|
|
||||||
|
|
||||||
def create_track_from_file(session, path, interactive=False):
|
def create_track_from_file(session, path, normalise=None, interactive=False):
|
||||||
"""
|
"""
|
||||||
Create track in database from passed path, or update database entry
|
Create track in database from passed path, or update database entry
|
||||||
if path already in database.
|
if path already in database.
|
||||||
@ -65,27 +69,28 @@ def create_track_from_file(session, path, interactive=False):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if interactive:
|
if interactive:
|
||||||
str = f"Importing {path}"
|
msg = f"Importing {path}"
|
||||||
INFO(str)
|
INFO(msg)
|
||||||
INFO("-" * len(str))
|
INFO("-" * len(msg))
|
||||||
INFO("Get track info...")
|
INFO("Get track info...")
|
||||||
t = get_music_info(path)
|
t = get_tags(path)
|
||||||
title = t['title']
|
title = t['title']
|
||||||
artist = t['artist']
|
artist = t['artist']
|
||||||
if interactive:
|
if interactive:
|
||||||
INFO(f" Title: \"{title}\"")
|
INFO(f" Title: \"{title}\"")
|
||||||
INFO(f" Artist: \"{artist}\"")
|
INFO(f" Artist: \"{artist}\"")
|
||||||
# Check for duplicate
|
# Check for duplicate
|
||||||
tracks = Tracks.search_titles(session, title)
|
if interactive:
|
||||||
if interactive and tracks:
|
tracks = Tracks.search_titles(session, title)
|
||||||
print("Found the following possible matches:")
|
if tracks:
|
||||||
for track in tracks:
|
print("Found the following possible matches:")
|
||||||
print(f'"{track.title}" by {track.artist}')
|
for track in tracks:
|
||||||
response = input("Continue [c] or abort [a]?")
|
print(f'"{track.title}" by {track.artist}')
|
||||||
if not response:
|
response = input("Continue [c] or abort [a]?")
|
||||||
return
|
if not response:
|
||||||
if response[0].lower() not in ['c', 'y']:
|
return
|
||||||
return
|
if response[0].lower() not in ['c', 'y']:
|
||||||
|
return
|
||||||
track = Tracks.get_or_create(session, path)
|
track = Tracks.get_or_create(session, path)
|
||||||
track.title = title
|
track.title = title
|
||||||
track.artist = artist
|
track.artist = artist
|
||||||
@ -103,7 +108,7 @@ def create_track_from_file(session, path, interactive=False):
|
|||||||
track.mtime = os.path.getmtime(path)
|
track.mtime = os.path.getmtime(path)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
if Config.NORMALISE_ON_IMPORT:
|
if normalise or normalise is None and Config.NORMALISE_ON_IMPORT:
|
||||||
if interactive:
|
if interactive:
|
||||||
INFO("Normalise...")
|
INFO("Normalise...")
|
||||||
# Check type
|
# Check type
|
||||||
@ -119,7 +124,7 @@ def create_track_from_file(session, path, interactive=False):
|
|||||||
fd, temp_path = tempfile.mkstemp()
|
fd, temp_path = tempfile.mkstemp()
|
||||||
shutil.copyfile(path, temp_path)
|
shutil.copyfile(path, temp_path)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
DEBUG(f"songdb.create_track_from_file({path}): err1: {str(err)}")
|
DEBUG(f"songdb.create_track_from_file({path}): err1: {repr(err)}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Overwrite original file with normalised output
|
# Overwrite original file with normalised output
|
||||||
@ -142,7 +147,7 @@ def create_track_from_file(session, path, interactive=False):
|
|||||||
dst[tag] = src[tag]
|
dst[tag] = src[tag]
|
||||||
dst.save()
|
dst.save()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
DEBUG(f"songdb.create_track_from_file({path}): err2: {str(err)}")
|
DEBUG(f"songdb.create_track_from_file({path}): err2: {repr(err)}")
|
||||||
# Restore original file
|
# Restore original file
|
||||||
shutil.copyfile(path, temp_path)
|
shutil.copyfile(path, temp_path)
|
||||||
finally:
|
finally:
|
||||||
@ -153,7 +158,7 @@ def create_track_from_file(session, path, interactive=False):
|
|||||||
|
|
||||||
|
|
||||||
def full_update_db(session):
|
def full_update_db(session):
|
||||||
"Rescan all entries in database"
|
"""Rescan all entries in database"""
|
||||||
|
|
||||||
def log(msg):
|
def log(msg):
|
||||||
INFO(f"full_update_db(): {msg}")
|
INFO(f"full_update_db(): {msg}")
|
||||||
@ -223,86 +228,6 @@ def full_update_db(session):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
def get_audio_segment(path):
|
|
||||||
try:
|
|
||||||
if path.endswith('.mp3'):
|
|
||||||
return AudioSegment.from_mp3(path)
|
|
||||||
elif path.endswith('.flac'):
|
|
||||||
return AudioSegment.from_file(path, "flac")
|
|
||||||
except AttributeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_music_info(path):
|
|
||||||
"""
|
|
||||||
Return a dictionary of title, artist, duration-in-milliseconds and path.
|
|
||||||
"""
|
|
||||||
|
|
||||||
tag = TinyTag.get(path)
|
|
||||||
|
|
||||||
return dict(
|
|
||||||
title=tag.title,
|
|
||||||
artist=tag.artist,
|
|
||||||
duration=tag.duration,
|
|
||||||
path=path
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def leading_silence(audio_segment, silence_threshold=Config.DBFS_SILENCE,
|
|
||||||
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
|
||||||
"""
|
|
||||||
Returns the millisecond/index that the leading silence ends.
|
|
||||||
audio_segment - the segment to find silence in
|
|
||||||
silence_threshold - the upper bound for how quiet is silent in dFBS
|
|
||||||
chunk_size - chunk size for interating over the segment in ms
|
|
||||||
|
|
||||||
https://github.com/jiaaro/pydub/blob/master/pydub/silence.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
trim_ms = 0 # ms
|
|
||||||
assert chunk_size > 0 # to avoid infinite loop
|
|
||||||
while (
|
|
||||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504
|
|
||||||
silence_threshold and trim_ms < len(audio_segment)):
|
|
||||||
trim_ms += chunk_size
|
|
||||||
|
|
||||||
# if there is no end it should return the length of the segment
|
|
||||||
return min(trim_ms, len(audio_segment))
|
|
||||||
|
|
||||||
|
|
||||||
def fade_point(audio_segment, fade_threshold=0,
|
|
||||||
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
|
||||||
"""
|
|
||||||
Returns the millisecond/index of the point where the volume drops below
|
|
||||||
the maximum and doesn't get louder again.
|
|
||||||
audio_segment - the sdlg_search_database_uiegment to find silence in
|
|
||||||
fade_threshold - the upper bound for how quiet is silent in dFBS
|
|
||||||
chunk_size - chunk size for interating over the segment in ms
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert chunk_size > 0 # to avoid infinite loop
|
|
||||||
|
|
||||||
segment_length = audio_segment.duration_seconds * 1000 # ms
|
|
||||||
trim_ms = segment_length - chunk_size
|
|
||||||
max_vol = audio_segment.dBFS
|
|
||||||
if fade_threshold == 0:
|
|
||||||
fade_threshold = max_vol
|
|
||||||
|
|
||||||
while (
|
|
||||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
|
|
||||||
and trim_ms > 0): # noqa W503
|
|
||||||
trim_ms -= chunk_size
|
|
||||||
|
|
||||||
# if there is no trailing silence, return lenght of track (it's less
|
|
||||||
# the chunk_size, but for chunk_size = 10ms, this may be ignored)
|
|
||||||
return int(trim_ms)
|
|
||||||
|
|
||||||
|
|
||||||
def trailing_silence(audio_segment, silence_threshold=-50.0,
|
|
||||||
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
|
||||||
return fade_point(audio_segment, silence_threshold, chunk_size)
|
|
||||||
|
|
||||||
|
|
||||||
def update_db(session):
|
def update_db(session):
|
||||||
"""
|
"""
|
||||||
Repopulate database
|
Repopulate database
|
||||||
@ -329,7 +254,7 @@ def update_db(session):
|
|||||||
for path in list(os_paths - db_paths):
|
for path in list(os_paths - db_paths):
|
||||||
DEBUG(f"songdb.update_db: {path=} not in database")
|
DEBUG(f"songdb.update_db: {path=} not in database")
|
||||||
# is filename in database?
|
# is filename in database?
|
||||||
track = Tracks.get_track_from_filename(session, os.path.basename(path))
|
track = Tracks.get_by_filename(session, os.path.basename(path))
|
||||||
if not track:
|
if not track:
|
||||||
messages.append(f"Track missing from database: {path}")
|
messages.append(f"Track missing from database: {path}")
|
||||||
else:
|
else:
|
||||||
@ -345,7 +270,7 @@ def update_db(session):
|
|||||||
# Remote any tracks from database whose paths don't exist
|
# Remote any tracks from database whose paths don't exist
|
||||||
for path in list(db_paths - os_paths):
|
for path in list(db_paths - os_paths):
|
||||||
# Manage tracks listed in database but where path is invalid
|
# Manage tracks listed in database but where path is invalid
|
||||||
track = Tracks.get_track_from_path(session, path)
|
track = Tracks.get_by_path(session, path)
|
||||||
messages.append(f"Remove from database: {path=} {track=}")
|
messages.append(f"Remove from database: {path=} {track=}")
|
||||||
|
|
||||||
# Remove references from Playdates
|
# Remove references from Playdates
|
||||||
@ -356,14 +281,15 @@ def update_db(session):
|
|||||||
f"File removed: {track.title=}, {track.artist=}, "
|
f"File removed: {track.title=}, {track.artist=}, "
|
||||||
f"{track.path=}"
|
f"{track.path=}"
|
||||||
)
|
)
|
||||||
for pt in PlaylistTracks.get_track_playlists(session, track.id):
|
for playlist in [a.playlist for a in track.playlists]:
|
||||||
# Create note
|
# Create note
|
||||||
Notes.add_note(session, pt.playlist_id, pt.row, note_txt)
|
Notes(session, playlist.id, pt.row, note_txt)
|
||||||
|
# TODO: this needs to call playlist.add_note() now
|
||||||
# Remove playlist entry
|
# Remove playlist entry
|
||||||
PlaylistTracks.remove_track(session, pt.playlist_id, pt.row)
|
playlist.remove_track(session, pt.row)
|
||||||
|
|
||||||
# Remove Track entry pointing to invalid path
|
# Remove Track entry pointing to invalid path
|
||||||
Tracks.remove_path(session, path)
|
Tracks.remove_by_path(session, path)
|
||||||
|
|
||||||
# Output messages (so if running via cron, these will get sent to
|
# Output messages (so if running via cron, these will get sent to
|
||||||
# user)
|
# user)
|
||||||
@ -372,44 +298,5 @@ def update_db(session):
|
|||||||
print("\n".join(messages))
|
print("\n".join(messages))
|
||||||
|
|
||||||
|
|
||||||
def update_meta(session, track, artist=None, title=None):
|
|
||||||
"""
|
|
||||||
Updates both the tag info in the file and the database entry with passed
|
|
||||||
artist and tag details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEBUG(f"songdb.update_meta({session=}, {track=}, {artist=}, {title=})")
|
|
||||||
|
|
||||||
if not artist and not title:
|
|
||||||
return
|
|
||||||
|
|
||||||
ftype = os.path.splitext(track.path)[1][1:]
|
|
||||||
if ftype == 'flac':
|
|
||||||
tag_handler = FLAC
|
|
||||||
elif ftype == 'mp3':
|
|
||||||
tag_handler = MP3
|
|
||||||
else:
|
|
||||||
INFO(f"File type {ftype} not implemented")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update tags
|
|
||||||
f = tag_handler(track.path)
|
|
||||||
try:
|
|
||||||
if artist:
|
|
||||||
f["artist"] = artist
|
|
||||||
if title:
|
|
||||||
f["title"] = title
|
|
||||||
f.save()
|
|
||||||
except TypeError:
|
|
||||||
show_warning("TAG error", "Can't update tag. Try editing in Audacity")
|
|
||||||
|
|
||||||
# Update database
|
|
||||||
with Session() as session:
|
|
||||||
if artist:
|
|
||||||
Tracks.update_artist(session, track.id, artist)
|
|
||||||
if title:
|
|
||||||
Tracks.update_title(session, track.id, title)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' and '__file__' in globals():
|
if __name__ == '__main__' and '__file__' in globals():
|
||||||
main()
|
main()
|
||||||
40
conftest.py
Normal file
40
conftest.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
sys.path.append("app")
|
||||||
|
import models
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def connection():
|
||||||
|
engine = create_engine(
|
||||||
|
"mysql+mysqldb://musicmuster_testing:musicmuster_testing@"
|
||||||
|
"localhost/musicmuster_testing"
|
||||||
|
)
|
||||||
|
return engine.connect()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def setup_database(connection):
|
||||||
|
from app.models import Base # noqa E402
|
||||||
|
Base.metadata.bind = connection
|
||||||
|
Base.metadata.create_all()
|
||||||
|
# seed_database()
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
Base.metadata.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def session(setup_database, connection):
|
||||||
|
transaction = connection.begin()
|
||||||
|
yield scoped_session(
|
||||||
|
sessionmaker(autocommit=False, autoflush=False, bind=connection)
|
||||||
|
)
|
||||||
|
transaction.rollback()
|
||||||
@ -24,7 +24,7 @@ fileConfig(config.config_file_name)
|
|||||||
path = os.path.dirname(os.path.dirname(__file__))
|
path = os.path.dirname(os.path.dirname(__file__))
|
||||||
sys.path.insert(0, path)
|
sys.path.insert(0, path)
|
||||||
sys.path.insert(0, os.path.join(path, "app"))
|
sys.path.insert(0, os.path.join(path, "app"))
|
||||||
from app.model import Base
|
from app.models import Base
|
||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
# other values from the config, defined by the needs of env.py,
|
# other values from the config, defined by the needs of env.py,
|
||||||
# can be acquired:
|
# can be acquired:
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
"""Add constraint to playlist_tracks
|
||||||
|
|
||||||
|
Revision ID: 1c4048efee96
|
||||||
|
Revises: 52cbded98e7c
|
||||||
|
Create Date: 2022-03-29 19:26:27.378185
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1c4048efee96'
|
||||||
|
down_revision = '52cbded98e7c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_unique_constraint('uniquerow', 'playlist_tracks', ['row', 'playlist_id'])
|
||||||
|
op.alter_column('playlists', 'loaded',
|
||||||
|
existing_type=mysql.TINYINT(display_width=1),
|
||||||
|
nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('playlists', 'loaded',
|
||||||
|
existing_type=mysql.TINYINT(display_width=1),
|
||||||
|
nullable=True)
|
||||||
|
op.drop_constraint('uniquerow', 'playlist_tracks', type_='unique')
|
||||||
|
# ### end Alembic commands ###
|
||||||
30
migrations/versions/52cbded98e7c_update_notecolours_table.py
Normal file
30
migrations/versions/52cbded98e7c_update_notecolours_table.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""Update notecolours table
|
||||||
|
|
||||||
|
Revision ID: 52cbded98e7c
|
||||||
|
Revises: c55992d1fe5f
|
||||||
|
Create Date: 2022-02-06 12:34:30.099417
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '52cbded98e7c'
|
||||||
|
down_revision = 'c55992d1fe5f'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('notecolours', sa.Column('colour', sa.String(length=21), nullable=True))
|
||||||
|
op.drop_column('notecolours', 'hexcolour')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('notecolours', sa.Column('hexcolour', mysql.VARCHAR(length=6), nullable=True))
|
||||||
|
op.drop_column('notecolours', 'colour')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
"""Add order to colours table
|
||||||
|
|
||||||
|
Revision ID: c55992d1fe5f
|
||||||
|
Revises: a5aada49f2fc
|
||||||
|
Create Date: 2022-02-05 21:28:36.391312
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c55992d1fe5f'
|
||||||
|
down_revision = 'a5aada49f2fc'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('notecolours', sa.Column('order', sa.Integer(), nullable=True))
|
||||||
|
op.create_index(op.f('ix_notecolours_enabled'), 'notecolours', ['enabled'], unique=False)
|
||||||
|
op.create_index(op.f('ix_notecolours_order'), 'notecolours', ['order'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_notecolours_order'), table_name='notecolours')
|
||||||
|
op.drop_index(op.f('ix_notecolours_enabled'), table_name='notecolours')
|
||||||
|
op.drop_column('notecolours', 'order')
|
||||||
|
# ### end Alembic commands ###
|
||||||
846
poetry.lock
generated
846
poetry.lock
generated
@ -1,6 +1,6 @@
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "alembic"
|
name = "alembic"
|
||||||
version = "1.7.5"
|
version = "1.7.7"
|
||||||
description = "A database migration tool for SQLAlchemy."
|
description = "A database migration tool for SQLAlchemy."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -13,6 +13,82 @@ SQLAlchemy = ">=1.3.0"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
tz = ["python-dateutil"]
|
tz = ["python-dateutil"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "appnope"
|
||||||
|
version = "0.1.2"
|
||||||
|
description = "Disable App Nap on macOS >= 10.9"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asttokens"
|
||||||
|
version = "2.0.5"
|
||||||
|
description = "Annotate AST trees with source code positions"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
six = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["astroid", "pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomicwrites"
|
||||||
|
version = "1.4.0"
|
||||||
|
description = "Atomic file writes."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "21.4.0"
|
||||||
|
description = "Classes Without Boilerplate"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
|
||||||
|
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||||
|
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
|
||||||
|
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backcall"
|
||||||
|
version = "0.2.0"
|
||||||
|
description = "Specifications for callback functions passed in to an API"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.4"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decorator"
|
||||||
|
version = "5.1.1"
|
||||||
|
description = "Decorators for Humans"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "executing"
|
||||||
|
version = "0.8.3"
|
||||||
|
description = "Get the currently executing AST node of a frame, and other information"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@ -24,13 +100,84 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["sphinx"]
|
docs = ["sphinx"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "1.1.1"
|
||||||
|
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipdb"
|
||||||
|
version = "0.13.9"
|
||||||
|
description = "IPython-enabled pdb"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
decorator = {version = "*", markers = "python_version > \"3.6\""}
|
||||||
|
ipython = {version = ">=7.17.0", markers = "python_version > \"3.6\""}
|
||||||
|
toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipython"
|
||||||
|
version = "8.1.1"
|
||||||
|
description = "IPython: Productive Interactive Computing"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
appnope = {version = "*", markers = "sys_platform == \"darwin\""}
|
||||||
|
backcall = "*"
|
||||||
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
decorator = "*"
|
||||||
|
jedi = ">=0.16"
|
||||||
|
matplotlib-inline = "*"
|
||||||
|
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
|
||||||
|
pickleshare = "*"
|
||||||
|
prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
|
||||||
|
pygments = ">=2.4.0"
|
||||||
|
stack-data = "*"
|
||||||
|
traitlets = ">=5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
all = ["black", "Sphinx (>=1.3)", "ipykernel", "nbconvert", "nbformat", "ipywidgets", "notebook", "ipyparallel", "qtconsole", "curio", "matplotlib (!=3.2.0)", "numpy (>=1.19)", "pandas", "pytest", "testpath", "trio", "pytest-asyncio"]
|
||||||
|
black = ["black"]
|
||||||
|
doc = ["Sphinx (>=1.3)"]
|
||||||
|
kernel = ["ipykernel"]
|
||||||
|
nbconvert = ["nbconvert"]
|
||||||
|
nbformat = ["nbformat"]
|
||||||
|
notebook = ["ipywidgets", "notebook"]
|
||||||
|
parallel = ["ipyparallel"]
|
||||||
|
qtconsole = ["qtconsole"]
|
||||||
|
test = ["pytest", "pytest-asyncio", "testpath"]
|
||||||
|
test_extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "pytest", "testpath", "trio"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jedi"
|
||||||
|
version = "0.18.1"
|
||||||
|
description = "An autocompletion tool for Python that can be used for text editors."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
parso = ">=0.8.0,<0.9.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
|
||||||
|
testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mako"
|
name = "mako"
|
||||||
version = "1.1.6"
|
version = "1.2.0"
|
||||||
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
MarkupSafe = ">=0.9.2"
|
MarkupSafe = ">=0.9.2"
|
||||||
@ -38,14 +185,26 @@ MarkupSafe = ">=0.9.2"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
babel = ["babel"]
|
babel = ["babel"]
|
||||||
lingua = ["lingua"]
|
lingua = ["lingua"]
|
||||||
|
testing = ["pytest"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markupsafe"
|
name = "markupsafe"
|
||||||
version = "2.0.1"
|
version = "2.1.0"
|
||||||
description = "Safely add untrusted strings to HTML/XML markup."
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matplotlib-inline"
|
||||||
|
version = "0.1.3"
|
||||||
|
description = "Inline Matplotlib backend for Jupyter"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
traitlets = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mutagen"
|
name = "mutagen"
|
||||||
@ -55,6 +214,31 @@ category = "main"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5, <4"
|
python-versions = ">=3.5, <4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "0.931"
|
||||||
|
description = "Optional static typing for Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mypy-extensions = ">=0.4.3"
|
||||||
|
tomli = ">=1.1.0"
|
||||||
|
typing-extensions = ">=3.10"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dmypy = ["psutil (>=4.0)"]
|
||||||
|
python2 = ["typed-ast (>=1.4.0,<2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "0.4.3"
|
||||||
|
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mysqlclient"
|
name = "mysqlclient"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
@ -63,6 +247,71 @@ category = "main"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "21.3"
|
||||||
|
description = "Core utilities for Python packages"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parso"
|
||||||
|
version = "0.8.3"
|
||||||
|
description = "A Python Parser"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
|
||||||
|
testing = ["docopt", "pytest (<6.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pexpect"
|
||||||
|
version = "4.8.0"
|
||||||
|
description = "Pexpect allows easy control of interactive console applications."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
ptyprocess = ">=0.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pickleshare"
|
||||||
|
version = "0.7.5"
|
||||||
|
description = "Tiny 'shelve'-like database with concurrency support"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "plugin and hook calling mechanisms for python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "tox"]
|
||||||
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prompt-toolkit"
|
||||||
|
version = "3.0.28"
|
||||||
|
description = "Library for building powerful interactive command lines in Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.2"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
wcwidth = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psutil"
|
name = "psutil"
|
||||||
version = "5.9.0"
|
version = "5.9.0"
|
||||||
@ -74,6 +323,33 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
|
test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ptyprocess"
|
||||||
|
version = "0.7.0"
|
||||||
|
description = "Run a subprocess in a pseudo terminal"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pure-eval"
|
||||||
|
version = "0.2.2"
|
||||||
|
description = "Safely evaluate AST nodes without side effects"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tests = ["pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "py"
|
||||||
|
version = "1.11.0"
|
||||||
|
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydub"
|
name = "pydub"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
@ -82,6 +358,25 @@ category = "main"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.11.2"
|
||||||
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyparsing"
|
||||||
|
version = "3.0.7"
|
||||||
|
description = "Python parsing module"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
diagrams = ["jinja2", "railroad-diagrams"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyqt5"
|
name = "pyqt5"
|
||||||
version = "5.15.6"
|
version = "5.15.6"
|
||||||
@ -104,12 +399,23 @@ python-versions = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyqt5-sip"
|
name = "pyqt5-sip"
|
||||||
version = "12.9.0"
|
version = "12.9.1"
|
||||||
description = "The sip module support for PyQt5"
|
description = "The sip module support for PyQt5"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyqt5-stubs"
|
||||||
|
version = "5.15.2.0"
|
||||||
|
description = "PEP561 stub files for the PyQt5 framework"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">= 3.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
build = ["docker (==4.2.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyqtwebengine"
|
name = "pyqtwebengine"
|
||||||
version = "5.15.5"
|
version = "5.15.5"
|
||||||
@ -131,17 +437,61 @@ category = "main"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "7.1.0"
|
||||||
|
description = "pytest: simple powerful testing with Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||||
|
attrs = ">=19.2.0"
|
||||||
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
iniconfig = "*"
|
||||||
|
packaging = "*"
|
||||||
|
pluggy = ">=0.12,<2.0"
|
||||||
|
py = ">=1.8.2"
|
||||||
|
tomli = ">=1.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-qt"
|
||||||
|
version = "4.0.2"
|
||||||
|
description = "pytest support for PyQt and PySide applications"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=3.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "tox"]
|
||||||
|
doc = ["sphinx", "sphinx-rtd-theme"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-vlc"
|
name = "python-vlc"
|
||||||
version = "3.0.12118"
|
version = "3.0.16120"
|
||||||
description = "VLC bindings for python."
|
description = "VLC bindings for python."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.16.0"
|
||||||
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "1.4.31"
|
version = "1.4.32"
|
||||||
description = "Database Abstraction Library"
|
description = "Database Abstraction Library"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -171,26 +521,129 @@ postgresql_psycopg2cffi = ["psycopg2cffi"]
|
|||||||
pymysql = ["pymysql (<1)", "pymysql"]
|
pymysql = ["pymysql (<1)", "pymysql"]
|
||||||
sqlcipher = ["sqlcipher3-binary"]
|
sqlcipher = ["sqlcipher3-binary"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlalchemy-stubs"
|
||||||
|
version = "0.4"
|
||||||
|
description = "SQLAlchemy stubs and mypy plugin"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mypy = ">=0.790"
|
||||||
|
typing-extensions = ">=3.7.4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stack-data"
|
||||||
|
version = "0.2.0"
|
||||||
|
description = "Extract data from python stack frames and tracebacks for informative displays"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
asttokens = "*"
|
||||||
|
executing = "*"
|
||||||
|
pure-eval = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tests = ["pytest", "typeguard", "pygments", "littleutils", "cython"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinytag"
|
name = "tinytag"
|
||||||
version = "1.7.0"
|
version = "1.8.1"
|
||||||
description = "Read music meta data and length of MP3, OGG, OPUS, MP4, M4A, FLAC, WMA and Wave files"
|
description = "Read music meta data and length of MP3, OGG, OPUS, MP4, M4A, FLAC, WMA and Wave files"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7"
|
python-versions = ">=2.7"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
tests = ["pytest", "pytest-cov", "coveralls", "flake8"]
|
tests = ["pytest", "pytest-cov", "flake8"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.10.2"
|
||||||
|
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.0.1"
|
||||||
|
description = "A lil' TOML parser"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "traitlets"
|
||||||
|
version = "5.1.1"
|
||||||
|
description = "Traitlets Python configuration system"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.1.1"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.6+"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wcwidth"
|
||||||
|
version = "0.2.5"
|
||||||
|
description = "Measures the displayed width of unicode strings in a terminal"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "28d15f944ff998687a9427f28c6c2006c7cbeefbe53193676a59a2f971b3c1a9"
|
content-hash = "08353ac7c54559da365ff26807f5179fefe388b62bc52884ee475a4a644e924a"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alembic = [
|
alembic = [
|
||||||
{file = "alembic-1.7.5-py3-none-any.whl", hash = "sha256:a9dde941534e3d7573d9644e8ea62a2953541e27bc1793e166f60b777ae098b4"},
|
{file = "alembic-1.7.7-py3-none-any.whl", hash = "sha256:29be0856ec7591c39f4e1cb10f198045d890e6e2274cf8da80cb5e721a09642b"},
|
||||||
{file = "alembic-1.7.5.tar.gz", hash = "sha256:7c328694a2e68f03ee971e63c3bd885846470373a5b532cf2c9f1601c413b153"},
|
{file = "alembic-1.7.7.tar.gz", hash = "sha256:4961248173ead7ce8a21efb3de378f13b8398e6630fab0eb258dc74a8af24c58"},
|
||||||
|
]
|
||||||
|
appnope = [
|
||||||
|
{file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"},
|
||||||
|
{file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
|
||||||
|
]
|
||||||
|
asttokens = [
|
||||||
|
{file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"},
|
||||||
|
{file = "asttokens-2.0.5.tar.gz", hash = "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5"},
|
||||||
|
]
|
||||||
|
atomicwrites = [
|
||||||
|
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
|
||||||
|
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
|
||||||
|
]
|
||||||
|
attrs = [
|
||||||
|
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
|
||||||
|
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
|
||||||
|
]
|
||||||
|
backcall = [
|
||||||
|
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
|
||||||
|
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
|
||||||
|
]
|
||||||
|
colorama = [
|
||||||
|
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||||
|
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||||
|
]
|
||||||
|
decorator = [
|
||||||
|
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
|
||||||
|
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
|
||||||
|
]
|
||||||
|
executing = [
|
||||||
|
{file = "executing-0.8.3-py2.py3-none-any.whl", hash = "sha256:d1eef132db1b83649a3905ca6dd8897f71ac6f8cac79a7e58a1a09cf137546c9"},
|
||||||
|
{file = "executing-0.8.3.tar.gz", hash = "sha256:c6554e21c6b060590a6d3be4b82fb78f8f0194d809de5ea7df1c093763311501"},
|
||||||
]
|
]
|
||||||
greenlet = [
|
greenlet = [
|
||||||
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
|
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
|
||||||
@ -249,85 +702,101 @@ greenlet = [
|
|||||||
{file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"},
|
{file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"},
|
||||||
{file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"},
|
{file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"},
|
||||||
]
|
]
|
||||||
|
iniconfig = [
|
||||||
|
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||||
|
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||||
|
]
|
||||||
|
ipdb = [
|
||||||
|
{file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"},
|
||||||
|
]
|
||||||
|
ipython = [
|
||||||
|
{file = "ipython-8.1.1-py3-none-any.whl", hash = "sha256:6f56bfaeaa3247aa3b9cd3b8cbab3a9c0abf7428392f97b21902d12b2f42a381"},
|
||||||
|
{file = "ipython-8.1.1.tar.gz", hash = "sha256:8138762243c9b3a3ffcf70b37151a2a35c23d3a29f9743878c33624f4207be3d"},
|
||||||
|
]
|
||||||
|
jedi = [
|
||||||
|
{file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"},
|
||||||
|
{file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"},
|
||||||
|
]
|
||||||
mako = [
|
mako = [
|
||||||
{file = "Mako-1.1.6-py2.py3-none-any.whl", hash = "sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57"},
|
{file = "Mako-1.2.0-py3-none-any.whl", hash = "sha256:23aab11fdbbb0f1051b93793a58323ff937e98e34aece1c4219675122e57e4ba"},
|
||||||
{file = "Mako-1.1.6.tar.gz", hash = "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2"},
|
{file = "Mako-1.2.0.tar.gz", hash = "sha256:9a7c7e922b87db3686210cf49d5d767033a41d4010b284e747682c92bddd8b39"},
|
||||||
]
|
]
|
||||||
markupsafe = [
|
markupsafe = [
|
||||||
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
|
{file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"},
|
||||||
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
|
{file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"},
|
||||||
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
|
{file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"},
|
||||||
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
|
{file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"},
|
||||||
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
|
{file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"},
|
||||||
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
|
{file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"},
|
||||||
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
|
{file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"},
|
||||||
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
|
{file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"},
|
||||||
{file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
|
{file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"},
|
||||||
{file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
|
{file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
|
{file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
|
{file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
|
{file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
|
{file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
|
{file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
|
{file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
|
{file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
|
{file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
|
{file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
|
{file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
|
{file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
|
{file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
|
{file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"},
|
||||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
|
{file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
|
{file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
|
{file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
|
{file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
|
{file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
|
{file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
|
{file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
|
{file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
|
{file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
|
{file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
|
{file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
|
{file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
|
{file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
|
{file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"},
|
||||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
|
{file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"},
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
|
{file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"},
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
|
{file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"},
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
|
]
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
|
matplotlib-inline = [
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
|
{file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
|
{file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"},
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
|
|
||||||
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
|
|
||||||
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
|
|
||||||
]
|
]
|
||||||
mutagen = [
|
mutagen = [
|
||||||
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
||||||
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
||||||
]
|
]
|
||||||
|
mypy = [
|
||||||
|
{file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"},
|
||||||
|
{file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"},
|
||||||
|
{file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"},
|
||||||
|
{file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"},
|
||||||
|
{file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"},
|
||||||
|
{file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"},
|
||||||
|
{file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"},
|
||||||
|
{file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"},
|
||||||
|
{file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"},
|
||||||
|
{file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"},
|
||||||
|
{file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"},
|
||||||
|
{file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"},
|
||||||
|
{file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"},
|
||||||
|
{file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"},
|
||||||
|
{file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"},
|
||||||
|
{file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"},
|
||||||
|
{file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"},
|
||||||
|
{file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"},
|
||||||
|
{file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"},
|
||||||
|
{file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"},
|
||||||
|
]
|
||||||
|
mypy-extensions = [
|
||||||
|
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||||
|
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||||
|
]
|
||||||
mysqlclient = [
|
mysqlclient = [
|
||||||
{file = "mysqlclient-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:02c8826e6add9b20f4cb12dcf016485f7b1d6e30356a1204d05431867a1b3947"},
|
{file = "mysqlclient-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:02c8826e6add9b20f4cb12dcf016485f7b1d6e30356a1204d05431867a1b3947"},
|
||||||
{file = "mysqlclient-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b62d23c11c516cedb887377c8807628c1c65d57593b57853186a6ee18b0c6a5b"},
|
{file = "mysqlclient-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b62d23c11c516cedb887377c8807628c1c65d57593b57853186a6ee18b0c6a5b"},
|
||||||
@ -335,6 +804,30 @@ mysqlclient = [
|
|||||||
{file = "mysqlclient-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14"},
|
{file = "mysqlclient-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14"},
|
||||||
{file = "mysqlclient-2.1.0.tar.gz", hash = "sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12"},
|
{file = "mysqlclient-2.1.0.tar.gz", hash = "sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12"},
|
||||||
]
|
]
|
||||||
|
packaging = [
|
||||||
|
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||||
|
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||||
|
]
|
||||||
|
parso = [
|
||||||
|
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
|
||||||
|
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
|
||||||
|
]
|
||||||
|
pexpect = [
|
||||||
|
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
|
||||||
|
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
|
||||||
|
]
|
||||||
|
pickleshare = [
|
||||||
|
{file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
|
||||||
|
{file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
|
||||||
|
]
|
||||||
|
pluggy = [
|
||||||
|
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||||
|
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||||
|
]
|
||||||
|
prompt-toolkit = [
|
||||||
|
{file = "prompt_toolkit-3.0.28-py3-none-any.whl", hash = "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c"},
|
||||||
|
{file = "prompt_toolkit-3.0.28.tar.gz", hash = "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"},
|
||||||
|
]
|
||||||
psutil = [
|
psutil = [
|
||||||
{file = "psutil-5.9.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b"},
|
{file = "psutil-5.9.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b"},
|
||||||
{file = "psutil-5.9.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618"},
|
{file = "psutil-5.9.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618"},
|
||||||
@ -369,10 +862,30 @@ psutil = [
|
|||||||
{file = "psutil-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3"},
|
{file = "psutil-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3"},
|
||||||
{file = "psutil-5.9.0.tar.gz", hash = "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25"},
|
{file = "psutil-5.9.0.tar.gz", hash = "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25"},
|
||||||
]
|
]
|
||||||
|
ptyprocess = [
|
||||||
|
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
|
||||||
|
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
|
||||||
|
]
|
||||||
|
pure-eval = [
|
||||||
|
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
|
||||||
|
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
|
||||||
|
]
|
||||||
|
py = [
|
||||||
|
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||||
|
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||||
|
]
|
||||||
pydub = [
|
pydub = [
|
||||||
{file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"},
|
{file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"},
|
||||||
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
|
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
|
||||||
]
|
]
|
||||||
|
pygments = [
|
||||||
|
{file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"},
|
||||||
|
{file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"},
|
||||||
|
]
|
||||||
|
pyparsing = [
|
||||||
|
{file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
|
||||||
|
{file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
|
||||||
|
]
|
||||||
pyqt5 = [
|
pyqt5 = [
|
||||||
{file = "PyQt5-5.15.6-cp36-abi3-macosx_10_13_x86_64.whl", hash = "sha256:33ced1c876f6a26e7899615a5a4efef2167c263488837c7beed023a64cebd051"},
|
{file = "PyQt5-5.15.6-cp36-abi3-macosx_10_13_x86_64.whl", hash = "sha256:33ced1c876f6a26e7899615a5a4efef2167c263488837c7beed023a64cebd051"},
|
||||||
{file = "PyQt5-5.15.6-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:9d6efad0377aa78bf081a20ac752ce86096ded18f04c592d98f5b92dc879ad0a"},
|
{file = "PyQt5-5.15.6-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:9d6efad0377aa78bf081a20ac752ce86096ded18f04c592d98f5b92dc879ad0a"},
|
||||||
@ -387,27 +900,31 @@ pyqt5-qt5 = [
|
|||||||
{file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"},
|
{file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"},
|
||||||
]
|
]
|
||||||
pyqt5-sip = [
|
pyqt5-sip = [
|
||||||
{file = "PyQt5_sip-12.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d5bca2fc222d58e8093ee8a81a6e3437067bb22bc3f86d06ec8be721e15e90a"},
|
{file = "PyQt5_sip-12.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b2e553e21b7ff124007a6b9168f8bb8c171fdf230d31ca0588df180f10bacbe"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:d59af63120d1475b2bf94fe8062610720a9be1e8940ea146c7f42bb449d49067"},
|
{file = "PyQt5_sip-12.9.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:5740a1770d6b92a5dca8bb0bda4620baf0d7a726beb864f69c667ddac91d6f64"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp310-cp310-win32.whl", hash = "sha256:0fc9aefacf502696710b36cdc9fa2a61487f55ee883dbcf2c2a6477e261546f7"},
|
{file = "PyQt5_sip-12.9.1-cp310-cp310-win32.whl", hash = "sha256:9699286fcdf4f75a4b91c7e4832c0f926af18d648c62a4ed72dd294c1a93705a"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:485972daff2fb0311013f471998f8ec8262ea381bded244f9d14edaad5f54271"},
|
{file = "PyQt5_sip-12.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:e2792af660da7479799f53028da88190ae8b4a0ad5acc2acbfd6c7bbfe110d58"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:d85002238b5180bce4b245c13d6face848faa1a7a9e5c6e292025004f2fd619a"},
|
{file = "PyQt5_sip-12.9.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:7ee08ad0ebf85b935f5d8d38306f8665fff9a6026c14fc0a7d780649e889c096"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:83c3220b1ca36eb8623ba2eb3766637b19eb0ce9f42336ad8253656d32750c0a"},
|
{file = "PyQt5_sip-12.9.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d21420b0739df2607864e2c80ca01994bc40cb116da6ad024ea8d9f407b0356"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp36-cp36m-win32.whl", hash = "sha256:d8b2bdff7bbf45bc975c113a03b14fd669dc0c73e1327f02706666a7dd51a197"},
|
{file = "PyQt5_sip-12.9.1-cp36-cp36m-win32.whl", hash = "sha256:ffd25051962c593d1c3c30188b9fbd8589ba17acd23a0202dc987bd3552fa611"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:69a3ad4259172e2b1aa9060de211efac39ddd734a517b1924d9c6c0cc4f55f96"},
|
{file = "PyQt5_sip-12.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:78ef8f1f41819661aa8e3117d6c1cd76fa14aef265e5bfd515dbfc64d412416b"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42274a501ab4806d2c31659170db14c282b8313d2255458064666d9e70d96206"},
|
{file = "PyQt5_sip-12.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5e641182bfee0501267c55e687832e4efe05becdae9e555d3695d706009b6598"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a8701892a01a5a2a4720872361197cc80fdd5f49c8482d488ddf38c9c84f055"},
|
{file = "PyQt5_sip-12.9.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c9a977d2835a5fbf250b00d61267dc228bdec9e20c7420d4e8d54d6f20410f87"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp37-cp37m-win32.whl", hash = "sha256:ac57d796c78117eb39edd1d1d1aea90354651efac9d3590aac67fa4983f99f1f"},
|
{file = "PyQt5_sip-12.9.1-cp37-cp37m-win32.whl", hash = "sha256:cec6ebf0b1163b18f09bc523160c467a9528b6dca129753827ac0bc432b332ae"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4347bd81d30c8e3181e553b3734f91658cfbdd8f1a19f254777f906870974e6d"},
|
{file = "PyQt5_sip-12.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:82c1b3080db7634fa318fddbb3cfaa30e63a67bca1001a76958c31f30b774a9d"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c446971c360a0a1030282a69375a08c78e8a61d568bfd6dab3dcc5cf8817f644"},
|
{file = "PyQt5_sip-12.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cfaad4a773c18b963092589b1a98153d36624601de8597a4dc287e5a295d5625"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fc43f2d7c438517ee33e929e8ae77132749c15909afab6aeece5fcf4147ffdb5"},
|
{file = "PyQt5_sip-12.9.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ce7a8b3af9db378c46b345d9809d481a74c4bfcd3129486c054fbdbc6a3503f9"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp38-cp38-win32.whl", hash = "sha256:055581c6fed44ba4302b70eeb82e979ff70400037358908f251cd85cbb3dbd93"},
|
{file = "PyQt5_sip-12.9.1-cp38-cp38-win32.whl", hash = "sha256:8fe5b3e4bbb8b472d05631cad21028d073f9f8eda770041449514cb3824a867f"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:c5216403d4d8d857ec4a61f631d3945e44fa248aa2415e9ee9369ab7c8a4d0c7"},
|
{file = "PyQt5_sip-12.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:5d59c4a5e856a35c41b47f5a23e1635b38cd1672f4f0122a68ebcb6889523ff2"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a25b9843c7da6a1608f310879c38e6434331aab1dc2fe6cb65c14f1ecf33780e"},
|
{file = "PyQt5_sip-12.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b56aedf7b0a496e4a8bd6087566888cea448aa01c76126cdb8b140e3ff3f5d93"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:dd05c768c2b55ffe56a9d49ce6cc77cdf3d53dbfad935258a9e347cbfd9a5850"},
|
{file = "PyQt5_sip-12.9.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:53e23dcc0fc3857204abd47660e383b930941bd1aeaf3c78ed59c5c12dd48010"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp39-cp39-win32.whl", hash = "sha256:4f8e05fe01d54275877c59018d8e82dcdd0bc5696053a8b830eecea3ce806121"},
|
{file = "PyQt5_sip-12.9.1-cp39-cp39-win32.whl", hash = "sha256:ee188eac5fd94dfe8d9e04a9e7fbda65c3535d5709278d8b7367ebd54f00e27f"},
|
||||||
{file = "PyQt5_sip-12.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:b09f4cd36a4831229fb77c424d89635fa937d97765ec90685e2f257e56a2685a"},
|
{file = "PyQt5_sip-12.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:989d51c41456cc496cb96f0b341464932b957040d26561f0bb4cf5a0914d6b36"},
|
||||||
{file = "PyQt5_sip-12.9.0.tar.gz", hash = "sha256:d3e4489d7c2b0ece9d203ae66e573939f7f60d4d29e089c9f11daa17cfeaae32"},
|
{file = "PyQt5_sip-12.9.1.tar.gz", hash = "sha256:2f24f299b44c511c23796aafbbb581bfdebf78d0905657b7cee2141b4982030e"},
|
||||||
|
]
|
||||||
|
pyqt5-stubs = [
|
||||||
|
{file = "PyQt5-stubs-5.15.2.0.tar.gz", hash = "sha256:dc0dea66f02fe297fb0cddd5767fbf58275b54ecb67f69ebeb994e1553bfb9b2"},
|
||||||
|
{file = "PyQt5_stubs-5.15.2.0-py3-none-any.whl", hash = "sha256:4b750d04ffca1bb188615d1a4e7d655a1d81d30df6ee3488f0adfe09b78e3d36"},
|
||||||
]
|
]
|
||||||
pyqtwebengine = [
|
pyqtwebengine = [
|
||||||
{file = "PyQtWebEngine-5.15.5-cp36-abi3-macosx_10_13_x86_64.whl", hash = "sha256:5c77f71d88d871bc7400c68ef6433fadc5bd57b86d1a9c4d8094cea42f3607f1"},
|
{file = "PyQtWebEngine-5.15.5-cp36-abi3-macosx_10_13_x86_64.whl", hash = "sha256:5c77f71d88d871bc7400c68ef6433fadc5bd57b86d1a9c4d8094cea42f3607f1"},
|
||||||
@ -422,48 +939,87 @@ pyqtwebengine-qt5 = [
|
|||||||
{file = "PyQtWebEngine_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9e80b408d8de09d4e708d5d84c3ceaf3603292ff8f5e566ae44bb0320fa59c33"},
|
{file = "PyQtWebEngine_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9e80b408d8de09d4e708d5d84c3ceaf3603292ff8f5e566ae44bb0320fa59c33"},
|
||||||
{file = "PyQtWebEngine_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:24231f19e1595018779977de6722b5c69f3d03f34a5f7574ff21cd1e764ef76d"},
|
{file = "PyQtWebEngine_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:24231f19e1595018779977de6722b5c69f3d03f34a5f7574ff21cd1e764ef76d"},
|
||||||
]
|
]
|
||||||
|
pytest = [
|
||||||
|
{file = "pytest-7.1.0-py3-none-any.whl", hash = "sha256:b555252a95bbb2a37a97b5ac2eb050c436f7989993565f5e0c9128fcaacadd0e"},
|
||||||
|
{file = "pytest-7.1.0.tar.gz", hash = "sha256:f1089d218cfcc63a212c42896f1b7fbf096874d045e1988186861a1a87d27b47"},
|
||||||
|
]
|
||||||
|
pytest-qt = [
|
||||||
|
{file = "pytest-qt-4.0.2.tar.gz", hash = "sha256:dfc5240dec7eb43b76bcb5f9a87eecae6ef83592af49f3af5f1d5d093acaa93e"},
|
||||||
|
{file = "pytest_qt-4.0.2-py2.py3-none-any.whl", hash = "sha256:e03847ac02a890ccaac0fde1748855b9dce425aceba62005c6cfced6cf7d5456"},
|
||||||
|
]
|
||||||
python-vlc = [
|
python-vlc = [
|
||||||
{file = "python-vlc-3.0.12118.tar.gz", hash = "sha256:566f2f7c303f6800851cacc016df1c6eeec094ad63e0a49d87db9d698094f1fb"},
|
{file = "python-vlc-3.0.16120.tar.gz", hash = "sha256:92f98fee088f72bd6d063b3b3312d0bd29b37e7ad65ddeb3a7303320300c2807"},
|
||||||
{file = "python_vlc-3.0.12118-py3-none-any.whl", hash = "sha256:f88be06c6f819a4db2de1c586b193b5df1410ff10fca33b8c6f4e56037c46f7b"},
|
{file = "python_vlc-3.0.16120-py3-none-any.whl", hash = "sha256:c409afb38fe9f788a663b4302ca583f31289ef0860ab2b1668da96bbe8f14bfc"},
|
||||||
|
]
|
||||||
|
six = [
|
||||||
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
]
|
]
|
||||||
sqlalchemy = [
|
sqlalchemy = [
|
||||||
{file = "SQLAlchemy-1.4.31-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c3abc34fed19fdeaead0ced8cf56dd121f08198008c033596aa6aae7cc58f59f"},
|
{file = "SQLAlchemy-1.4.32-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:4b2bcab3a914715d332ca783e9bda13bc570d8b9ef087563210ba63082c18c16"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8d0949b11681380b4a50ac3cd075e4816afe9fa4a8c8ae006c1ca26f0fa40ad8"},
|
{file = "SQLAlchemy-1.4.32-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:159c2f69dd6efd28e894f261ffca1100690f28210f34cfcd70b895e0ea7a64f3"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp27-cp27m-win32.whl", hash = "sha256:f3b7ec97e68b68cb1f9ddb82eda17b418f19a034fa8380a0ac04e8fe01532875"},
|
{file = "SQLAlchemy-1.4.32-cp27-cp27m-win_amd64.whl", hash = "sha256:d7e483f4791fbda60e23926b098702340504f7684ce7e1fd2c1bf02029288423"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp27-cp27m-win_amd64.whl", hash = "sha256:81f2dd355b57770fdf292b54f3e0a9823ec27a543f947fa2eb4ec0df44f35f0d"},
|
{file = "SQLAlchemy-1.4.32-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4aa96e957141006181ca58e792e900ee511085b8dae06c2d08c00f108280fb8a"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4ad31cec8b49fd718470328ad9711f4dc703507d434fd45461096da0a7135ee0"},
|
{file = "SQLAlchemy-1.4.32-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:576684771456d02e24078047c2567025f2011977aa342063468577d94e194b00"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:05fa14f279d43df68964ad066f653193187909950aa0163320b728edfc400167"},
|
{file = "SQLAlchemy-1.4.32-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff677fa4522dafb5a5e2c0cf909790d5d367326321aeabc0dffc9047cb235bd"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dccff41478050e823271642837b904d5f9bda3f5cf7d371ce163f00a694118d6"},
|
{file = "SQLAlchemy-1.4.32-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8679f9aba5ac22e7bce54ccd8a77641d3aea3e2d96e73e4356c887ebf8ff1082"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57205844f246bab9b666a32f59b046add8995c665d9ecb2b7b837b087df90639"},
|
{file = "SQLAlchemy-1.4.32-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7046f7aa2db445daccc8424f50b47a66c4039c9f058246b43796aa818f8b751"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8210090a816d48a4291a47462bac750e3bc5c2442e6d64f7b8137a7c3f9ac5"},
|
{file = "SQLAlchemy-1.4.32-cp310-cp310-win32.whl", hash = "sha256:bedd89c34ab62565d44745212814e4b57ef1c24ad4af9b29c504ce40f0dc6558"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp310-cp310-win32.whl", hash = "sha256:2e216c13ecc7fcdcbb86bb3225425b3ed338e43a8810c7089ddb472676124b9b"},
|
{file = "SQLAlchemy-1.4.32-cp310-cp310-win_amd64.whl", hash = "sha256:199dc6d0068753b6a8c0bd3aceb86a3e782df118260ebc1fa981ea31ee054674"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp310-cp310-win_amd64.whl", hash = "sha256:e3a86b59b6227ef72ffc10d4b23f0fe994bef64d4667eab4fb8cd43de4223bec"},
|
{file = "SQLAlchemy-1.4.32-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8e1e5d96b744a4f91163290b01045430f3f32579e46d87282449e5b14d27d4ac"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2fd4d3ca64c41dae31228b80556ab55b6489275fb204827f6560b65f95692cf3"},
|
{file = "SQLAlchemy-1.4.32-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edfcf93fd92e2f9eef640b3a7a40db20fe3c1d7c2c74faa41424c63dead61b76"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f22c040d196f841168b1456e77c30a18a3dc16b336ddbc5a24ce01ab4e95ae0"},
|
{file = "SQLAlchemy-1.4.32-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04164e0063feb7aedd9d073db0fd496edb244be40d46ea1f0d8990815e4b8c34"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0c7171aa5a57e522a04a31b84798b6c926234cb559c0939840c3235cf068813"},
|
{file = "SQLAlchemy-1.4.32-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba59761c19b800bc2e1c9324da04d35ef51e4ee9621ff37534bc2290d258f71"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d046a9aeba9bc53e88a41e58beb72b6205abb9a20f6c136161adf9128e589db5"},
|
{file = "SQLAlchemy-1.4.32-cp36-cp36m-win32.whl", hash = "sha256:708973b5d9e1e441188124aaf13c121e5b03b6054c2df59b32219175a25aa13e"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp36-cp36m-win32.whl", hash = "sha256:d86132922531f0dc5a4f424c7580a472a924dd737602638e704841c9cb24aea2"},
|
{file = "SQLAlchemy-1.4.32-cp36-cp36m-win_amd64.whl", hash = "sha256:316270e5867566376e69a0ac738b863d41396e2b63274616817e1d34156dff0e"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp36-cp36m-win_amd64.whl", hash = "sha256:ca68c52e3cae491ace2bf39b35fef4ce26c192fd70b4cd90f040d419f70893b5"},
|
{file = "SQLAlchemy-1.4.32-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9a0195af6b9050c9322a97cf07514f66fe511968e623ca87b2df5e3cf6349615"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:cf2cd387409b12d0a8b801610d6336ee7d24043b6dd965950eaec09b73e7262f"},
|
{file = "SQLAlchemy-1.4.32-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e4a3c0c3c596296b37f8427c467c8e4336dc8d50f8ed38042e8ba79507b2c9"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb4b15fb1f0aafa65cbdc62d3c2078bea1ceecbfccc9a1f23a2113c9ac1191fa"},
|
{file = "SQLAlchemy-1.4.32-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bca714d831e5b8860c3ab134c93aec63d1a4f493bed20084f54e3ce9f0a3bf99"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c317ddd7c586af350a6aef22b891e84b16bff1a27886ed5b30f15c1ed59caeaa"},
|
{file = "SQLAlchemy-1.4.32-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9a680d9665f88346ed339888781f5236347933906c5a56348abb8261282ec48"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c7ed6c69debaf6198fadb1c16ae1253a29a7670bbf0646f92582eb465a0b999"},
|
{file = "SQLAlchemy-1.4.32-cp37-cp37m-win32.whl", hash = "sha256:9cb5698c896fa72f88e7ef04ef62572faf56809093180771d9be8d9f2e264a13"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp37-cp37m-win32.whl", hash = "sha256:6a01ec49ca54ce03bc14e10de55dfc64187a2194b3b0e5ac0fdbe9b24767e79e"},
|
{file = "SQLAlchemy-1.4.32-cp37-cp37m-win_amd64.whl", hash = "sha256:8b9a395122770a6f08ebfd0321546d7379f43505882c7419d7886856a07caa13"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp37-cp37m-win_amd64.whl", hash = "sha256:330eb45395874cc7787214fdd4489e2afb931bc49e0a7a8f9cd56d6e9c5b1639"},
|
{file = "SQLAlchemy-1.4.32-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:3f88a4ee192142eeed3fe173f673ea6ab1f5a863810a9d85dbf6c67a9bd08f97"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:5e9c7b3567edbc2183607f7d9f3e7e89355b8f8984eec4d2cd1e1513c8f7b43f"},
|
{file = "SQLAlchemy-1.4.32-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd93162615870c976dba43963a24bb418b28448fef584f30755990c134a06a55"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de85c26a5a1c72e695ab0454e92f60213b4459b8d7c502e0be7a6369690eeb1a"},
|
{file = "SQLAlchemy-1.4.32-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5a2e73508f939175363d8a4be9dcdc84cf16a92578d7fa86e6e4ca0e6b3667b2"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:975f5c0793892c634c4920057da0de3a48bbbbd0a5c86f5fcf2f2fedf41b76da"},
|
{file = "SQLAlchemy-1.4.32-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfec934aac7f9fa95fc82147a4ba5db0a8bdc4ebf1e33b585ab8860beb10232f"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5c20c8415173b119762b6110af64448adccd4d11f273fb9f718a9865b88a99c"},
|
{file = "SQLAlchemy-1.4.32-cp38-cp38-win32.whl", hash = "sha256:bb42f9b259c33662c6a9b866012f6908a91731a419e69304e1261ba3ab87b8d1"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp38-cp38-win32.whl", hash = "sha256:b35dca159c1c9fa8a5f9005e42133eed82705bf8e243da371a5e5826440e65ca"},
|
{file = "SQLAlchemy-1.4.32-cp38-cp38-win_amd64.whl", hash = "sha256:7ff72b3cc9242d1a1c9b84bd945907bf174d74fc2519efe6184d6390a8df478b"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp38-cp38-win_amd64.whl", hash = "sha256:b7b20c88873675903d6438d8b33fba027997193e274b9367421e610d9da76c08"},
|
{file = "SQLAlchemy-1.4.32-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5dc9801ae9884e822ba942ca493642fb50f049c06b6dbe3178691fce48ceb089"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:85e4c244e1de056d48dae466e9baf9437980c19fcde493e0db1a0a986e6d75b4"},
|
{file = "SQLAlchemy-1.4.32-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4607d2d16330757818c9d6fba322c2e80b4b112ff24295d1343a80b876eb0ed"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79e73d5ee24196d3057340e356e6254af4d10e1fc22d3207ea8342fc5ffb977"},
|
{file = "SQLAlchemy-1.4.32-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:20e9eba7fd86ef52e0df25bea83b8b518dfdf0bce09b336cfe51671f52aaaa3f"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:15a03261aa1e68f208e71ae3cd845b00063d242cbf8c87348a0c2c0fc6e1f2ac"},
|
{file = "SQLAlchemy-1.4.32-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:290cbdf19129ae520d4bdce392648c6fcdbee763bc8f750b53a5ab51880cb9c9"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ddc5e5ccc0160e7ad190e5c61eb57560f38559e22586955f205e537cda26034"},
|
{file = "SQLAlchemy-1.4.32-cp39-cp39-win32.whl", hash = "sha256:1bbac3e8293b34c4403d297e21e8f10d2a57756b75cff101dc62186adec725f5"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp39-cp39-win32.whl", hash = "sha256:289465162b1fa1e7a982f8abe59d26a8331211cad4942e8031d2b7db1f75e649"},
|
{file = "SQLAlchemy-1.4.32-cp39-cp39-win_amd64.whl", hash = "sha256:b3f1d9b3aa09ab9adc7f8c4b40fc3e081eb903054c9a6f9ae1633fe15ae503b4"},
|
||||||
{file = "SQLAlchemy-1.4.31-cp39-cp39-win_amd64.whl", hash = "sha256:9e4fb2895b83993831ba2401b6404de953fdbfa9d7d4fa6a4756294a83bbc94f"},
|
{file = "SQLAlchemy-1.4.32.tar.gz", hash = "sha256:6fdd2dc5931daab778c2b65b03df6ae68376e028a3098eb624d0909d999885bc"},
|
||||||
{file = "SQLAlchemy-1.4.31.tar.gz", hash = "sha256:582b59d1e5780a447aada22b461e50b404a9dc05768da1d87368ad8190468418"},
|
]
|
||||||
|
sqlalchemy-stubs = [
|
||||||
|
{file = "sqlalchemy-stubs-0.4.tar.gz", hash = "sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae"},
|
||||||
|
{file = "sqlalchemy_stubs-0.4-py3-none-any.whl", hash = "sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5"},
|
||||||
|
]
|
||||||
|
stack-data = [
|
||||||
|
{file = "stack_data-0.2.0-py3-none-any.whl", hash = "sha256:999762f9c3132308789affa03e9271bbbe947bf78311851f4d485d8402ed858e"},
|
||||||
|
{file = "stack_data-0.2.0.tar.gz", hash = "sha256:45692d41bd633a9503a5195552df22b583caf16f0b27c4e58c98d88c8b648e12"},
|
||||||
]
|
]
|
||||||
tinytag = [
|
tinytag = [
|
||||||
{file = "tinytag-1.7.0.tar.gz", hash = "sha256:513135c156e93026837a177ed336487f4fbdaa5c7efe8b8bfbeab9ce8f1404f7"},
|
{file = "tinytag-1.8.1.tar.gz", hash = "sha256:363ab3107831a5598b68aaa061aba915fb1c7b4254d770232e65d5db8487636d"},
|
||||||
|
]
|
||||||
|
toml = [
|
||||||
|
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||||
|
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||||
|
]
|
||||||
|
tomli = [
|
||||||
|
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||||
|
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||||
|
]
|
||||||
|
traitlets = [
|
||||||
|
{file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"},
|
||||||
|
{file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"},
|
||||||
|
]
|
||||||
|
typing-extensions = [
|
||||||
|
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
|
||||||
|
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
|
||||||
|
]
|
||||||
|
wcwidth = [
|
||||||
|
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||||
|
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
prof/combined.prof
Normal file
BIN
prof/combined.prof
Normal file
Binary file not shown.
3833
prof/combined.svg
Normal file
3833
prof/combined.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 262 KiB |
BIN
prof/test_get_selected_row.prof
Normal file
BIN
prof/test_get_selected_row.prof
Normal file
Binary file not shown.
@ -16,9 +16,15 @@ alembic = "^1.7.5"
|
|||||||
psutil = "^5.9.0"
|
psutil = "^5.9.0"
|
||||||
PyQtWebEngine = "^5.15.5"
|
PyQtWebEngine = "^5.15.5"
|
||||||
pydub = "^0.25.1"
|
pydub = "^0.25.1"
|
||||||
PyQt5-sip = "^12.9.0"
|
PyQt5-sip = "^12.9.1"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
|
ipdb = "^0.13.9"
|
||||||
|
sqlalchemy-stubs = "^0.4"
|
||||||
|
PyQt5-stubs = "^5.15.2"
|
||||||
|
mypy = "^0.931"
|
||||||
|
pytest = "^7.0.1"
|
||||||
|
pytest-qt = "^4.0.2"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|||||||
2
pytest.ini
Normal file
2
pytest.ini
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[pytest]
|
||||||
|
addopts = -xls
|
||||||
72
test_helpers.py
Normal file
72
test_helpers.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
from config import Config
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from helpers import *
|
||||||
|
from models import Tracks
|
||||||
|
|
||||||
|
|
||||||
|
def test_fade_point():
|
||||||
|
test_track_path = "testdata/isa.mp3"
|
||||||
|
test_track_data = "testdata/isa.py"
|
||||||
|
|
||||||
|
audio_segment = get_audio_segment(test_track_path)
|
||||||
|
assert audio_segment
|
||||||
|
|
||||||
|
fade_at = fade_point(audio_segment)
|
||||||
|
|
||||||
|
# Get test data
|
||||||
|
with open(test_track_data) as f:
|
||||||
|
testdata = eval(f.read())
|
||||||
|
|
||||||
|
# Volume detection can vary, so ± 1 second is OK
|
||||||
|
assert fade_at < testdata['fade_at'] + 1000
|
||||||
|
assert fade_at > testdata['fade_at'] - 1000
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_tags():
|
||||||
|
test_track_path = "testdata/mom.mp3"
|
||||||
|
test_track_data = "testdata/mom.py"
|
||||||
|
|
||||||
|
tags = get_tags(test_track_path)
|
||||||
|
|
||||||
|
# Get test data
|
||||||
|
with open(test_track_data) as f:
|
||||||
|
testdata = eval(f.read())
|
||||||
|
|
||||||
|
assert tags['artist'] == testdata['artist']
|
||||||
|
assert tags['title'] == testdata['title']
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_relative_date():
|
||||||
|
assert get_relative_date(None) == "Never"
|
||||||
|
today_at_10 = datetime.now().replace(hour=10, minute=0)
|
||||||
|
today_at_11 = datetime.now().replace(hour=11, minute=0)
|
||||||
|
assert get_relative_date(today_at_10, today_at_11) == "10:00"
|
||||||
|
eight_days_ago = today_at_10 - timedelta(days=8)
|
||||||
|
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
|
||||||
|
sixteen_days_ago = today_at_10 - timedelta(days=16)
|
||||||
|
assert get_relative_date(
|
||||||
|
sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
|
||||||
|
|
||||||
|
|
||||||
|
def test_leading_silence():
|
||||||
|
test_track_path = "testdata/isa.mp3"
|
||||||
|
test_track_data = "testdata/isa.py"
|
||||||
|
|
||||||
|
audio_segment = get_audio_segment(test_track_path)
|
||||||
|
assert audio_segment
|
||||||
|
|
||||||
|
silence_at = leading_silence(audio_segment)
|
||||||
|
|
||||||
|
# Get test data
|
||||||
|
with open(test_track_data) as f:
|
||||||
|
testdata = eval(f.read())
|
||||||
|
|
||||||
|
# Volume detection can vary, so ± 1 second is OK
|
||||||
|
assert silence_at < testdata['leading_silence'] + 1000
|
||||||
|
assert silence_at > testdata['leading_silence'] - 1000
|
||||||
|
|
||||||
|
|
||||||
|
def test_ms_to_mmss():
|
||||||
|
assert ms_to_mmss(None) == "-"
|
||||||
|
assert ms_to_mmss(59600) == "0:59"
|
||||||
|
assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23"
|
||||||
513
test_models.py
Normal file
513
test_models.py
Normal file
@ -0,0 +1,513 @@
|
|||||||
|
import os.path
|
||||||
|
|
||||||
|
from app.models import (
|
||||||
|
NoteColours,
|
||||||
|
Notes,
|
||||||
|
Playdates,
|
||||||
|
Playlists,
|
||||||
|
PlaylistTracks,
|
||||||
|
Tracks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_notecolours_get_colour(session):
|
||||||
|
"""Create a colour record and retrieve all colours"""
|
||||||
|
|
||||||
|
note_colour = "#0bcdef"
|
||||||
|
NoteColours(session, substring="substring", colour=note_colour)
|
||||||
|
|
||||||
|
records = NoteColours.get_all(session)
|
||||||
|
assert len(records) == 1
|
||||||
|
record = records[0]
|
||||||
|
assert record.colour == note_colour
|
||||||
|
|
||||||
|
|
||||||
|
def test_notecolours_get_all(session):
|
||||||
|
"""Create two colour records and retrieve them all"""
|
||||||
|
|
||||||
|
note1_colour = "#1bcdef"
|
||||||
|
note2_colour = "#20ff00"
|
||||||
|
NoteColours(session, substring="note1", colour=note1_colour)
|
||||||
|
NoteColours(session, substring="note2", colour=note2_colour)
|
||||||
|
|
||||||
|
records = NoteColours.get_all(session)
|
||||||
|
assert len(records) == 2
|
||||||
|
assert note1_colour in [n.colour for n in records]
|
||||||
|
assert note2_colour in [n.colour for n in records]
|
||||||
|
|
||||||
|
|
||||||
|
def test_notecolours_get_colour_none(session):
|
||||||
|
note_colour = "#3bcdef"
|
||||||
|
NoteColours(session, substring="substring", colour=note_colour)
|
||||||
|
|
||||||
|
result = NoteColours.get_colour(session, "xyz")
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_notecolours_get_colour_match(session):
|
||||||
|
note_colour = "#4bcdef"
|
||||||
|
nc = NoteColours(session, substring="sub", colour=note_colour)
|
||||||
|
assert nc
|
||||||
|
|
||||||
|
result = NoteColours.get_colour(session, "The substring")
|
||||||
|
assert result == note_colour
|
||||||
|
|
||||||
|
|
||||||
|
def test_notes_creation(session):
|
||||||
|
# We need a playlist
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
|
||||||
|
note_text = "note text"
|
||||||
|
note = Notes(session, playlist.id, 0, note_text)
|
||||||
|
assert note
|
||||||
|
|
||||||
|
notes = session.query(Notes).all()
|
||||||
|
assert len(notes) == 1
|
||||||
|
assert notes[0].note == note_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_notes_delete(session):
|
||||||
|
# We need a playlist
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
|
||||||
|
note_text = "note text"
|
||||||
|
note = Notes(session, playlist.id, 0, note_text)
|
||||||
|
assert note
|
||||||
|
|
||||||
|
notes = session.query(Notes).all()
|
||||||
|
assert len(notes) == 1
|
||||||
|
assert notes[0].note == note_text
|
||||||
|
|
||||||
|
note.delete_note(session)
|
||||||
|
notes = session.query(Notes).all()
|
||||||
|
assert len(notes) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_notes_update_row_only(session):
|
||||||
|
# We need a playlist
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
|
||||||
|
note_text = "note text"
|
||||||
|
note = Notes(session, playlist.id, 0, note_text)
|
||||||
|
new_row = 10
|
||||||
|
|
||||||
|
note.update_note(session, new_row)
|
||||||
|
|
||||||
|
notes = session.query(Notes).all()
|
||||||
|
assert len(notes) == 1
|
||||||
|
assert notes[0].row == new_row
|
||||||
|
|
||||||
|
|
||||||
|
def test_notes_update_text(session):
|
||||||
|
# We need a playlist
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
|
||||||
|
note_text = "note text"
|
||||||
|
note = Notes(session, playlist.id, 0, note_text)
|
||||||
|
new_text = "This is new"
|
||||||
|
new_row = 0
|
||||||
|
|
||||||
|
note.update_note(session, new_row, new_text)
|
||||||
|
notes = session.query(Notes).all()
|
||||||
|
|
||||||
|
assert len(notes) == 1
|
||||||
|
assert notes[0].note == new_text
|
||||||
|
assert notes[0].row == new_row
|
||||||
|
|
||||||
|
|
||||||
|
def test_playdates_add_playdate(session):
|
||||||
|
"""Test playdate and last_played retrieval"""
|
||||||
|
|
||||||
|
# We need a track
|
||||||
|
track_path = "/a/b/c"
|
||||||
|
track = Tracks(session, track_path)
|
||||||
|
|
||||||
|
playdate = Playdates(session, track.id)
|
||||||
|
assert playdate
|
||||||
|
|
||||||
|
last_played = Playdates.last_played(session, track.id)
|
||||||
|
assert abs((playdate.lastplayed - last_played).total_seconds()) < 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_playdates_remove_track(session):
|
||||||
|
"""Test removing a track from a playdate"""
|
||||||
|
|
||||||
|
# We need a track
|
||||||
|
track_path = "/a/b/c"
|
||||||
|
track = Tracks(session, track_path)
|
||||||
|
|
||||||
|
playdate = Playdates(session, track.id)
|
||||||
|
Playdates.remove_track(session, track.id)
|
||||||
|
|
||||||
|
last_played = Playdates.last_played(session, track.id)
|
||||||
|
assert last_played is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_create(session):
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
assert playlist
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_add_note(session):
|
||||||
|
note_text = "my note"
|
||||||
|
note_row = 2
|
||||||
|
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
note = playlist.add_note(session, note_row, note_text)
|
||||||
|
|
||||||
|
assert len(playlist.notes) == 1
|
||||||
|
playlist_note = playlist.notes[0]
|
||||||
|
assert playlist_note.note == note_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_add_track(session):
|
||||||
|
# We need a playlist
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
|
||||||
|
# We need a track
|
||||||
|
track_path = "/a/b/c"
|
||||||
|
track = Tracks(session, track_path)
|
||||||
|
|
||||||
|
row = 17
|
||||||
|
|
||||||
|
playlist.add_track(session, track.id, row)
|
||||||
|
|
||||||
|
assert len(playlist.tracks) == 1
|
||||||
|
playlist_track = playlist.tracks[row]
|
||||||
|
assert playlist_track.path == track_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_tracks(session):
|
||||||
|
# We need a playlist
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
|
||||||
|
# We need two tracks
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1_row = 17
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
|
||||||
|
track2_path = "/x/y/z"
|
||||||
|
track2_row = 29
|
||||||
|
track2 = Tracks(session, track2_path)
|
||||||
|
|
||||||
|
playlist.add_track(session, track1.id, track1_row)
|
||||||
|
playlist.add_track(session, track2.id, track2_row)
|
||||||
|
|
||||||
|
tracks = playlist.tracks
|
||||||
|
assert tracks[track1_row] == track1
|
||||||
|
assert tracks[track2_row] == track2
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_notes(session):
|
||||||
|
# We need a playlist
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
|
||||||
|
# We need two notes
|
||||||
|
note1_text = "note1 text"
|
||||||
|
note1_row = 11
|
||||||
|
note1 = Notes(session, playlist.id, note1_row, note1_text)
|
||||||
|
|
||||||
|
note2_text = "note2 text"
|
||||||
|
note2_row = 19
|
||||||
|
note2 = Notes(session, playlist.id, note2_row, note2_text)
|
||||||
|
|
||||||
|
notes = playlist.notes
|
||||||
|
assert note1_text in [n.note for n in notes]
|
||||||
|
assert note1_row in [n.row for n in notes]
|
||||||
|
assert note2_text in [n.note for n in notes]
|
||||||
|
assert note2_row in [n.row for n in notes]
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_open_and_close(session):
|
||||||
|
# We need a playlist
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
|
||||||
|
assert len(Playlists.get_open(session)) == 1
|
||||||
|
assert len(Playlists.get_closed(session)) == 0
|
||||||
|
|
||||||
|
playlist.close(session)
|
||||||
|
|
||||||
|
assert len(Playlists.get_open(session)) == 0
|
||||||
|
assert len(Playlists.get_closed(session)) == 1
|
||||||
|
|
||||||
|
playlist.mark_open(session)
|
||||||
|
|
||||||
|
assert len(Playlists.get_open(session)) == 1
|
||||||
|
assert len(Playlists.get_closed(session)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_get_all_and_by_id(session):
|
||||||
|
# We need two playlists
|
||||||
|
p1_name = "playlist one"
|
||||||
|
p2_name = "playlist two"
|
||||||
|
playlist1 = Playlists(session, p1_name)
|
||||||
|
_ = Playlists(session, p2_name)
|
||||||
|
|
||||||
|
all_playlists = Playlists.get_all(session)
|
||||||
|
assert len(all_playlists) == 2
|
||||||
|
assert p1_name in [p.name for p in all_playlists]
|
||||||
|
assert p2_name in [p.name for p in all_playlists]
|
||||||
|
assert Playlists.get_by_id(session, playlist1.id).name == p1_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_remove_tracks(session):
|
||||||
|
# Need two playlists and three tracks
|
||||||
|
p1_name = "playlist one"
|
||||||
|
playlist1 = Playlists(session, p1_name)
|
||||||
|
p2_name = "playlist two"
|
||||||
|
playlist2 = Playlists(session, p2_name)
|
||||||
|
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
track2_path = "/m/n/o"
|
||||||
|
track2 = Tracks(session, track2_path)
|
||||||
|
track3_path = "/x/y/z"
|
||||||
|
track3 = Tracks(session, track3_path)
|
||||||
|
|
||||||
|
# Add all tracks to both playlists
|
||||||
|
for p in [playlist1, playlist2]:
|
||||||
|
for t in [track1, track2, track3]:
|
||||||
|
p.add_track(session, t.id)
|
||||||
|
|
||||||
|
assert len(playlist1.tracks) == 3
|
||||||
|
assert len(playlist2.tracks) == 3
|
||||||
|
|
||||||
|
playlist1.remove_track(session, 1)
|
||||||
|
assert len(playlist1.tracks) == 2
|
||||||
|
# Check the track itself still exists
|
||||||
|
original_track = Tracks.get_by_id(session, track1.id)
|
||||||
|
assert original_track
|
||||||
|
|
||||||
|
playlist1.remove_all_tracks(session)
|
||||||
|
assert len(playlist1.tracks) == 0
|
||||||
|
assert len(playlist2.tracks) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_get_track_playlists(session):
|
||||||
|
# Need two playlists and two tracks
|
||||||
|
p1_name = "playlist one"
|
||||||
|
playlist1 = Playlists(session, p1_name)
|
||||||
|
p2_name = "playlist two"
|
||||||
|
playlist2 = Playlists(session, p2_name)
|
||||||
|
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
track2_path = "/m/n/o"
|
||||||
|
track2 = Tracks(session, track2_path)
|
||||||
|
|
||||||
|
# Put track1 in both playlists, track2 only in playlist1
|
||||||
|
playlist1.add_track(session, track1.id)
|
||||||
|
playlist2.add_track(session, track1.id)
|
||||||
|
playlist1.add_track(session, track2.id)
|
||||||
|
|
||||||
|
playlists_track1 = track1.playlists
|
||||||
|
playlists_track2 = track2.playlists
|
||||||
|
assert p1_name in [a.playlist.name for a in playlists_track1]
|
||||||
|
assert p2_name in [a.playlist.name for a in playlists_track1]
|
||||||
|
assert p1_name in [a.playlist.name for a in playlists_track2]
|
||||||
|
assert p2_name not in [a.playlist.name for a in playlists_track2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_move_track(session):
|
||||||
|
# We need two playlists
|
||||||
|
p1_name = "playlist one"
|
||||||
|
p2_name = "playlist two"
|
||||||
|
playlist1 = Playlists(session, p1_name)
|
||||||
|
playlist2 = Playlists(session, p2_name)
|
||||||
|
|
||||||
|
# Need two tracks
|
||||||
|
track1_row = 17
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
track2_row = 29
|
||||||
|
track2_path = "/m/n/o"
|
||||||
|
track2 = Tracks(session, track2_path)
|
||||||
|
|
||||||
|
# Add both to playlist1 and check
|
||||||
|
playlist1.add_track(session, track1.id, track1_row)
|
||||||
|
playlist1.add_track(session, track2.id, track2_row)
|
||||||
|
|
||||||
|
tracks = playlist1.tracks
|
||||||
|
assert tracks[track1_row] == track1
|
||||||
|
assert tracks[track2_row] == track2
|
||||||
|
|
||||||
|
# Move track2 to playlist2 and check
|
||||||
|
playlist1.move_track(session, [track2_row], playlist2)
|
||||||
|
|
||||||
|
tracks1 = playlist1.tracks
|
||||||
|
tracks2 = playlist2.tracks
|
||||||
|
assert len(tracks1) == 1
|
||||||
|
assert len(tracks2) == 1
|
||||||
|
assert tracks1[track1_row] == track1
|
||||||
|
assert tracks2[0] == track2
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_get_all_paths(session):
|
||||||
|
# Need two tracks
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
_ = Tracks(session, track1_path)
|
||||||
|
track2_path = "/m/n/o"
|
||||||
|
_ = Tracks(session, track2_path)
|
||||||
|
|
||||||
|
result = Tracks.get_all_paths(session)
|
||||||
|
assert track1_path in result
|
||||||
|
assert track2_path in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_get_all_tracks(session):
|
||||||
|
# Need two tracks
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
track2_path = "/m/n/o"
|
||||||
|
track2 = Tracks(session, track2_path)
|
||||||
|
|
||||||
|
result = Tracks.get_all_tracks(session)
|
||||||
|
assert track1_path in [a.path for a in result]
|
||||||
|
assert track2_path in [a.path for a in result]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_get_or_create(session):
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
|
||||||
|
track1 = Tracks.get_or_create(session, track1_path)
|
||||||
|
assert track1.path == track1_path
|
||||||
|
track2 = Tracks.get_or_create(session, track1_path)
|
||||||
|
assert track1 is track2
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_by_filename(session):
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
assert Tracks.get_by_filename(
|
||||||
|
session, os.path.basename(track1_path)
|
||||||
|
) is track1
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_by_path(session):
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
assert Tracks.get_by_path(session, track1_path) is track1
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_by_id(session):
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
assert Tracks.get_by_id(session, track1.id) is track1
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_rescan(session):
|
||||||
|
# Get test track
|
||||||
|
test_track_path = "./testdata/isa.mp3"
|
||||||
|
test_track_data = "./testdata/isa.py"
|
||||||
|
|
||||||
|
track = Tracks(session, test_track_path)
|
||||||
|
track.rescan(session)
|
||||||
|
|
||||||
|
# Get test data
|
||||||
|
with open(test_track_data) as f:
|
||||||
|
testdata = eval(f.read())
|
||||||
|
|
||||||
|
# Re-read the track
|
||||||
|
track_read = Tracks.get_by_path(session, test_track_path)
|
||||||
|
|
||||||
|
assert track_read.duration == testdata['duration']
|
||||||
|
assert track_read.start_gap == testdata['leading_silence']
|
||||||
|
# Silence detection can vary, so ± 1 second is OK
|
||||||
|
assert track_read.fade_at < testdata['fade_at'] + 1000
|
||||||
|
assert track_read.fade_at > testdata['fade_at'] - 1000
|
||||||
|
assert track_read.silence_at < testdata['trailing_silence'] + 1000
|
||||||
|
assert track_read.silence_at > testdata['trailing_silence'] - 1000
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_remove_by_path(session):
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
assert len(Tracks.get_all_tracks(session)) == 1
|
||||||
|
Tracks.remove_by_path(session, track1_path)
|
||||||
|
assert len(Tracks.get_all_tracks(session)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_search_artists(session):
|
||||||
|
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1_artist = "Artist One"
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
track1.artist = track1_artist
|
||||||
|
|
||||||
|
track2_path = "/m/n/o"
|
||||||
|
track2_artist = "Artist Two"
|
||||||
|
track2 = Tracks(session, track2_path)
|
||||||
|
track2.artist = track2_artist
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
x = Tracks.get_all_tracks(session)
|
||||||
|
artist_first_word = track1_artist.split()[0].lower()
|
||||||
|
assert len(Tracks.search_artists(session, artist_first_word)) == 2
|
||||||
|
assert len(Tracks.search_artists(session, track1_artist)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_search_titles(session):
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1_title = "Title One"
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
track1.title = track1_title
|
||||||
|
|
||||||
|
track2_path = "/m/n/o"
|
||||||
|
track2_title = "Title Two"
|
||||||
|
track2 = Tracks(session, track2_path)
|
||||||
|
track2.title = track2_title
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
x = Tracks.get_all_tracks(session)
|
||||||
|
title_first_word = track1_title.split()[0].lower()
|
||||||
|
assert len(Tracks.search_titles(session, title_first_word)) == 2
|
||||||
|
assert len(Tracks.search_titles(session, track1_title)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_update_lastplayed(session):
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1 = Tracks(session, track1_path)
|
||||||
|
|
||||||
|
assert track1.lastplayed is None
|
||||||
|
track1.update_lastplayed(session, track1.id)
|
||||||
|
assert track1.lastplayed is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_update_info(session):
|
||||||
|
path = "/a/b/c"
|
||||||
|
artist = "The Beatles"
|
||||||
|
title = "Help!"
|
||||||
|
newinfo = "abcdef"
|
||||||
|
|
||||||
|
track1 = Tracks(session, path)
|
||||||
|
track1.artist = artist
|
||||||
|
track1.title = title
|
||||||
|
|
||||||
|
test1 = Tracks.get_by_id(session, track1.id)
|
||||||
|
assert test1.artist == artist
|
||||||
|
assert test1.title == title
|
||||||
|
assert test1.path == path
|
||||||
|
|
||||||
|
track1.path = newinfo
|
||||||
|
test2 = Tracks.get_by_id(session, track1.id)
|
||||||
|
assert test2.artist == artist
|
||||||
|
assert test2.title == title
|
||||||
|
assert test2.path == newinfo
|
||||||
|
|
||||||
|
track1.artist = newinfo
|
||||||
|
test2 = Tracks.get_by_id(session, track1.id)
|
||||||
|
assert test2.artist == newinfo
|
||||||
|
assert test2.title == title
|
||||||
|
assert test2.path == newinfo
|
||||||
|
|
||||||
|
track1.title = newinfo
|
||||||
|
test3 = Tracks.get_by_id(session, track1.id)
|
||||||
|
assert test3.artist == newinfo
|
||||||
|
assert test3.title == newinfo
|
||||||
|
assert test3.path == newinfo
|
||||||
296
test_playlists.py
Normal file
296
test_playlists.py
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
from app import playlists
|
||||||
|
from app import models
|
||||||
|
from app import musicmuster
|
||||||
|
from app import dbconfig
|
||||||
|
|
||||||
|
|
||||||
|
def seed2tracks(session):
|
||||||
|
tracks = [
|
||||||
|
{
|
||||||
|
"path": "testdata/isa.mp3",
|
||||||
|
"title": "I'm so afraid",
|
||||||
|
"artist": "Fleetwood Mac",
|
||||||
|
"duration": 263000,
|
||||||
|
"start_gap": 60,
|
||||||
|
"fade_at": 236263,
|
||||||
|
"silence_at": 260343,
|
||||||
|
"mtime": 371900000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "testdata/mom.mp3",
|
||||||
|
"title": "Man of Mystery",
|
||||||
|
"artist": "The Shadows",
|
||||||
|
"duration": 120000,
|
||||||
|
"start_gap": 70,
|
||||||
|
"fade_at": 115000,
|
||||||
|
"silence_at": 118000,
|
||||||
|
"mtime": 1642760000,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for track in tracks:
|
||||||
|
db_track = models.Tracks(session=session, **track)
|
||||||
|
session.add(db_track)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_init(qtbot, session):
|
||||||
|
"""Just check we can create a playlist_tab"""
|
||||||
|
|
||||||
|
playlist = models.Playlists(session, "my playlist")
|
||||||
|
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
assert playlist_tab
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_and_restore(qtbot, session):
|
||||||
|
"""Playlist with one track, one note, save and restore"""
|
||||||
|
|
||||||
|
# Create playlist
|
||||||
|
playlist = models.Playlists(session, "my playlist")
|
||||||
|
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
|
||||||
|
# Insert a note
|
||||||
|
note_text = "my note"
|
||||||
|
note_row = 7
|
||||||
|
note = models.Notes(session, playlist.id, note_row, note_text)
|
||||||
|
playlist_tab._insert_note(session, note)
|
||||||
|
|
||||||
|
# Add a track
|
||||||
|
track_path = "/a/b/c"
|
||||||
|
track = models.Tracks(session, track_path)
|
||||||
|
# Inserting the track will also save the playlist
|
||||||
|
playlist_tab.insert_track(session, track)
|
||||||
|
|
||||||
|
# We need to commit the session before re-querying
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Retrieve playlist
|
||||||
|
all_playlists = playlists.Playlists.get_open(session)
|
||||||
|
assert len(all_playlists) == 1
|
||||||
|
retrieved_playlist = all_playlists[0]
|
||||||
|
paths = [a.path for a in retrieved_playlist.tracks.values()]
|
||||||
|
assert track_path in paths
|
||||||
|
notes = [a.note for a in retrieved_playlist.notes]
|
||||||
|
assert note_text in notes
|
||||||
|
|
||||||
|
|
||||||
|
def test_meta_all_clear(qtbot, session):
|
||||||
|
|
||||||
|
# Create playlist
|
||||||
|
playlist = models.Playlists(session, "my playlist")
|
||||||
|
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
|
||||||
|
# Add some tracks
|
||||||
|
# Need to commit session after each one so that new row is found
|
||||||
|
# for subsequent inserts
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1 = models.Tracks(session, track1_path)
|
||||||
|
playlist_tab.insert_track(session, track1)
|
||||||
|
session.commit()
|
||||||
|
track2_path = "/d/e/f"
|
||||||
|
track2 = models.Tracks(session, track2_path)
|
||||||
|
playlist_tab.insert_track(session, track2)
|
||||||
|
session.commit()
|
||||||
|
track3_path = "/h/i/j"
|
||||||
|
track3 = models.Tracks(session, track3_path)
|
||||||
|
playlist_tab.insert_track(session, track3)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
assert playlist_tab._get_current_track_row() is None
|
||||||
|
assert playlist_tab._get_next_track_row() is None
|
||||||
|
assert playlist_tab._get_notes_rows() == []
|
||||||
|
assert playlist_tab._get_played_track_rows() == []
|
||||||
|
assert len(playlist_tab._get_unreadable_track_rows()) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_meta(qtbot, session):
|
||||||
|
|
||||||
|
# Create playlist
|
||||||
|
playlist = playlists.Playlists(session, "my playlist")
|
||||||
|
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
|
||||||
|
# Add some tracks
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1 = models.Tracks(session, track1_path)
|
||||||
|
playlist_tab.insert_track(session, track1)
|
||||||
|
session.commit()
|
||||||
|
track2_path = "/d/e/f"
|
||||||
|
track2 = models.Tracks(session, track2_path)
|
||||||
|
playlist_tab.insert_track(session, track2)
|
||||||
|
session.commit()
|
||||||
|
track3_path = "/h/i/j"
|
||||||
|
track3 = models.Tracks(session, track3_path)
|
||||||
|
playlist_tab.insert_track(session, track3)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
assert len(playlist_tab._get_unreadable_track_rows()) == 3
|
||||||
|
|
||||||
|
assert playlist_tab._get_played_track_rows() == []
|
||||||
|
assert playlist_tab._get_current_track_row() is None
|
||||||
|
assert playlist_tab._get_next_track_row() is None
|
||||||
|
assert playlist_tab._get_notes_rows() == []
|
||||||
|
|
||||||
|
playlist_tab._set_played_row(0)
|
||||||
|
assert playlist_tab._get_played_track_rows() == [0]
|
||||||
|
assert playlist_tab._get_current_track_row() is None
|
||||||
|
assert playlist_tab._get_next_track_row() is None
|
||||||
|
assert playlist_tab._get_notes_rows() == []
|
||||||
|
|
||||||
|
# Add a note
|
||||||
|
note_text = "my note"
|
||||||
|
note_row = 7 # will be added as row 3
|
||||||
|
note = models.Notes(session, playlist.id, note_row, note_text)
|
||||||
|
playlist_tab._insert_note(session, note)
|
||||||
|
|
||||||
|
assert playlist_tab._get_played_track_rows() == [0]
|
||||||
|
assert playlist_tab._get_current_track_row() is None
|
||||||
|
assert playlist_tab._get_next_track_row() is None
|
||||||
|
assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
playlist_tab._set_next_track_row(1)
|
||||||
|
assert playlist_tab._get_played_track_rows() == [0]
|
||||||
|
assert playlist_tab._get_current_track_row() is None
|
||||||
|
assert playlist_tab._get_next_track_row() == 1
|
||||||
|
assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
playlist_tab._set_current_track_row(2)
|
||||||
|
assert playlist_tab._get_played_track_rows() == [0]
|
||||||
|
assert playlist_tab._get_current_track_row() == 2
|
||||||
|
assert playlist_tab._get_next_track_row() == 1
|
||||||
|
assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
playlist_tab._clear_played_row_status(0)
|
||||||
|
assert playlist_tab._get_played_track_rows() == []
|
||||||
|
assert playlist_tab._get_current_track_row() == 2
|
||||||
|
assert playlist_tab._get_next_track_row() == 1
|
||||||
|
assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
playlist_tab._meta_clear_next()
|
||||||
|
assert playlist_tab._get_played_track_rows() == []
|
||||||
|
assert playlist_tab._get_current_track_row() == 2
|
||||||
|
assert playlist_tab._get_next_track_row() is None
|
||||||
|
assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
playlist_tab._clear_current_track_row()
|
||||||
|
assert playlist_tab._get_played_track_rows() == []
|
||||||
|
assert playlist_tab._get_current_track_row() is None
|
||||||
|
assert playlist_tab._get_next_track_row() is None
|
||||||
|
assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
# Test clearing again has no effect
|
||||||
|
playlist_tab._clear_current_track_row()
|
||||||
|
assert playlist_tab._get_played_track_rows() == []
|
||||||
|
assert playlist_tab._get_current_track_row() is None
|
||||||
|
assert playlist_tab._get_next_track_row() is None
|
||||||
|
assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_next(qtbot, session):
|
||||||
|
# Create playlist
|
||||||
|
playlist = models.Playlists(session, "my playlist")
|
||||||
|
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
|
||||||
|
# Add some tracks
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1 = models.Tracks(session, track1_path)
|
||||||
|
playlist_tab.insert_track(session, track1)
|
||||||
|
session.commit()
|
||||||
|
track2_path = "/d/e/f"
|
||||||
|
track2 = models.Tracks(session, track2_path)
|
||||||
|
playlist_tab.insert_track(session, track2)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
playlist_tab._set_next_track_row(1)
|
||||||
|
assert playlist_tab._get_next_track_row() == 1
|
||||||
|
|
||||||
|
playlist_tab.clear_next(session)
|
||||||
|
assert playlist_tab._get_next_track_row() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_selected_row(qtbot, monkeypatch, session):
|
||||||
|
|
||||||
|
monkeypatch.setattr(musicmuster, "Session", session)
|
||||||
|
monkeypatch.setattr(playlists, "Session", session)
|
||||||
|
|
||||||
|
# Create playlist and playlist_tab
|
||||||
|
window = musicmuster.Window()
|
||||||
|
playlist = models.Playlists(session, "test playlist")
|
||||||
|
playlist_tab = playlists.PlaylistTab(window, session, playlist.id)
|
||||||
|
|
||||||
|
# Add some tracks
|
||||||
|
track1_path = "/a/b/c"
|
||||||
|
track1 = models.Tracks(session, track1_path)
|
||||||
|
playlist_tab.insert_track(session, track1)
|
||||||
|
session.commit()
|
||||||
|
track2_path = "/d/e/f"
|
||||||
|
track2 = models.Tracks(session, track2_path)
|
||||||
|
playlist_tab.insert_track(session, track2)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
qtbot.addWidget(playlist_tab)
|
||||||
|
with qtbot.waitExposed(window):
|
||||||
|
window.show()
|
||||||
|
row0_item0 = playlist_tab.item(0, 0)
|
||||||
|
assert row0_item0 is not None
|
||||||
|
rect = playlist_tab.visualItemRect(row0_item0)
|
||||||
|
qtbot.mouseClick(
|
||||||
|
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
|
||||||
|
)
|
||||||
|
row_number = playlist_tab.get_selected_row()
|
||||||
|
assert row_number == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_next(qtbot, monkeypatch, session):
|
||||||
|
|
||||||
|
monkeypatch.setattr(musicmuster, "Session", session)
|
||||||
|
monkeypatch.setattr(playlists, "Session", session)
|
||||||
|
seed2tracks(session)
|
||||||
|
|
||||||
|
playlist_name = "test playlist"
|
||||||
|
# Create testing playlist
|
||||||
|
window = musicmuster.Window()
|
||||||
|
playlist = models.Playlists(session, playlist_name)
|
||||||
|
playlist_tab = playlists.PlaylistTab(window, session, playlist.id)
|
||||||
|
idx = window.tabPlaylist.addTab(playlist_tab, playlist_name)
|
||||||
|
window.tabPlaylist.setCurrentIndex(idx)
|
||||||
|
qtbot.addWidget(playlist_tab)
|
||||||
|
|
||||||
|
# Add some tracks
|
||||||
|
track1 = models.Tracks.get_by_filename(session, "isa.mp3")
|
||||||
|
track1_title = track1.title
|
||||||
|
assert track1_title
|
||||||
|
|
||||||
|
playlist_tab.insert_track(session, track1)
|
||||||
|
session.commit()
|
||||||
|
track2 = models.Tracks.get_by_filename(session, "mom.mp3")
|
||||||
|
playlist_tab.insert_track(session, track2)
|
||||||
|
|
||||||
|
with qtbot.waitExposed(window):
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
row0_item2 = playlist_tab.item(0, 2)
|
||||||
|
assert row0_item2 is not None
|
||||||
|
rect = playlist_tab.visualItemRect(row0_item2)
|
||||||
|
qtbot.mouseClick(
|
||||||
|
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
|
||||||
|
)
|
||||||
|
selected_title = playlist_tab.get_selected_title()
|
||||||
|
assert selected_title == track1_title
|
||||||
|
|
||||||
|
qtbot.keyPress(playlist_tab.viewport(), "N",
|
||||||
|
modifier=Qt.ControlModifier)
|
||||||
|
qtbot.wait(1000)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kae(monkeypatch, session):
|
||||||
|
# monkeypatch.setattr(dbconfig, "Session", session)
|
||||||
|
monkeypatch.setattr(musicmuster, "Session", session)
|
||||||
|
|
||||||
|
musicmuster.Window.kae()
|
||||||
|
# monkeypatch.setattr(musicmuster, "Session", session)
|
||||||
|
# monkeypatch.setattr(dbconfig, "Session", session)
|
||||||
|
# monkeypatch.setattr(models, "Session", session)
|
||||||
|
# monkeypatch.setattr(playlists, "Session", session)
|
||||||
8
testdata/isa.py
vendored
Normal file
8
testdata/isa.py
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Measurements for isa.{mp3,flac} (milliseconds)
|
||||||
|
|
||||||
|
{
|
||||||
|
"leading_silence": 60,
|
||||||
|
"fade_at": 236163,
|
||||||
|
"trailing_silence": 259373,
|
||||||
|
"duration": 262533,
|
||||||
|
}
|
||||||
6
testdata/mom.py
vendored
Normal file
6
testdata/mom.py
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Tags for mom.py
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Man of Mystery",
|
||||||
|
"artist": "The Shadows",
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user