mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
first recommendation draft
This commit is contained in:
@@ -14,6 +14,7 @@ from app.config import UserConfig
|
|||||||
from app.db.userdata import UserTable
|
from app.db.userdata import UserTable
|
||||||
from app.settings import Info as AppInfo
|
from app.settings import Info as AppInfo
|
||||||
from .plugins import lyrics as lyrics_plugin
|
from .plugins import lyrics as lyrics_plugin
|
||||||
|
from .plugins import mixes as mixes_plugin
|
||||||
from app.api import (
|
from app.api import (
|
||||||
album,
|
album,
|
||||||
artist,
|
artist,
|
||||||
@@ -113,6 +114,7 @@ def create_api():
|
|||||||
# Plugins
|
# Plugins
|
||||||
app.register_api(plugins.api)
|
app.register_api(plugins.api)
|
||||||
app.register_api(lyrics_plugin.api)
|
app.register_api(lyrics_plugin.api)
|
||||||
|
app.register_api(mixes_plugin.api)
|
||||||
|
|
||||||
# Logger
|
# Logger
|
||||||
app.register_api(scrobble.api)
|
app.register_api(scrobble.api)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from flask_openapi3 import APIBlueprint
|
|||||||
from app.api.apischemas import GenericLimitSchema
|
from app.api.apischemas import GenericLimitSchema
|
||||||
from app.lib.home.recentlyadded import get_recently_added_items
|
from app.lib.home.recentlyadded import get_recently_added_items
|
||||||
from app.lib.home.recentlyplayed import get_recently_played
|
from app.lib.home.recentlyplayed import get_recently_played
|
||||||
|
from app.store.homepage import HomepageStore
|
||||||
|
|
||||||
bp_tag = Tag(name="Home", description="Homepage items")
|
bp_tag = Tag(name="Home", description="Homepage items")
|
||||||
api = APIBlueprint("home", __name__, url_prefix="/home", abp_tags=[bp_tag])
|
api = APIBlueprint("home", __name__, url_prefix="/home", abp_tags=[bp_tag])
|
||||||
@@ -24,3 +25,15 @@ def get_recent_plays(query: GenericLimitSchema):
|
|||||||
Get recently played
|
Get recently played
|
||||||
"""
|
"""
|
||||||
return {"items": get_recently_played(query.limit)}
|
return {"items": get_recently_played(query.limit)}
|
||||||
|
|
||||||
|
|
||||||
|
@api.get("/")
|
||||||
|
def homepage_items():
|
||||||
|
return {
|
||||||
|
"artist_mixes": {
|
||||||
|
"title": "Artist mixes for you",
|
||||||
|
"description": "Curated based on artists you have played in the past",
|
||||||
|
"items": HomepageStore.get_artist_mixes(),
|
||||||
|
"extra": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
from flask_openapi3 import Tag
|
||||||
|
from flask_openapi3 import APIBlueprint
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from app.plugins.mixes import MixesPlugin
|
||||||
|
from app.store.homepage import HomepageStore
|
||||||
|
from app.store.tracks import TrackStore
|
||||||
|
|
||||||
|
|
||||||
|
bp_tag = Tag(name="Mixes Plugin", description="Mixes plugin hehe")
|
||||||
|
api = APIBlueprint(
|
||||||
|
"mixesplugin", __name__, url_prefix="/plugins/mixes", abp_tags=[bp_tag]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api.post("/track")
|
||||||
|
def get_track_mix():
|
||||||
|
"""
|
||||||
|
Get a track mix
|
||||||
|
"""
|
||||||
|
mixes = MixesPlugin()
|
||||||
|
track = TrackStore.trackhashmap["9eeee292264ad01b"].get_best()
|
||||||
|
tracks = mixes.get_track_mix(track)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(tracks),
|
||||||
|
"tracks": tracks,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@api.post("/artist")
|
||||||
|
def get_artist_mix():
|
||||||
|
mixes = MixesPlugin()
|
||||||
|
return mixes.get_artists()
|
||||||
|
# tracks = mixes.get_artist_mix("09306be8039b98ad")
|
||||||
|
|
||||||
|
# return {
|
||||||
|
# "total": len(tracks),
|
||||||
|
# "tracks": tracks,
|
||||||
|
# }
|
||||||
|
|
||||||
|
return "hi"
|
||||||
|
|
||||||
|
|
||||||
|
class MixQuery(BaseModel):
|
||||||
|
mixid: str = Field(description="The mix id")
|
||||||
|
|
||||||
|
|
||||||
|
@api.get("/")
|
||||||
|
def get_mix(query: MixQuery):
|
||||||
|
mixtype = ""
|
||||||
|
|
||||||
|
match query.mixid[0]:
|
||||||
|
case "a":
|
||||||
|
mixtype = "artist_mixes"
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Invalid mix ID: {query.mixid}")
|
||||||
|
|
||||||
|
mix = HomepageStore.get_mix(mixtype, query.mixid[1:])
|
||||||
|
if mix:
|
||||||
|
return mix, 200
|
||||||
|
|
||||||
|
return {"msg": "Mix not found"}, 404
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import time
|
||||||
|
import schedule
|
||||||
|
|
||||||
|
from app.crons.mixes import Mixes
|
||||||
|
from app.utils.threading import background
|
||||||
|
|
||||||
|
|
||||||
|
@background
|
||||||
|
def start_cron_jobs():
|
||||||
|
Mixes().run()
|
||||||
|
|
||||||
|
# schedule.run_pending()
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import schedule
|
||||||
|
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class CronJob(ABC):
|
||||||
|
"""
|
||||||
|
A cron job that will be run on a regular interval.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, hours: int):
|
||||||
|
self.name = name
|
||||||
|
self.hours = hours
|
||||||
|
|
||||||
|
schedule.every(self.hours).seconds.do(self.run)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
The function that will be called by the cron job.
|
||||||
|
"""
|
||||||
|
...
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from app.crons.cron import CronJob
|
||||||
|
from app.plugins.mixes import MixesPlugin
|
||||||
|
from app.store.homepage import HomepageStore
|
||||||
|
|
||||||
|
|
||||||
|
class Mixes(CronJob):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("mixes", 5)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
print("⭐⭐⭐⭐ Mixes cron job running")
|
||||||
|
mixes = MixesPlugin()
|
||||||
|
artist_mixes = mixes.get_artists()
|
||||||
|
|
||||||
|
HomepageStore.set_artist_mixes(artist_mixes)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
import time
|
||||||
|
|
||||||
|
from app.lib.playlistlib import get_first_4_images
|
||||||
|
from app.serializers.track import serialize_tracks
|
||||||
|
from app.store.tracks import TrackStore
|
||||||
|
from app.utils.dates import seconds_to_time_string
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Mix:
|
||||||
|
id: str
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
tracks: list[str]
|
||||||
|
|
||||||
|
timestamp: int = field(default_factory=lambda: int(time.time()))
|
||||||
|
extra: dict = field(default_factory=dict)
|
||||||
|
saved: bool = False
|
||||||
|
|
||||||
|
def to_full_dict(self):
|
||||||
|
tracks = TrackStore.get_tracks_by_trackhashes(self.tracks)
|
||||||
|
serialized_tracks = serialize_tracks(tracks)
|
||||||
|
|
||||||
|
_dict = asdict(self)
|
||||||
|
_dict["tracks"] = serialized_tracks
|
||||||
|
_dict["images"] = get_first_4_images(tracks)
|
||||||
|
_dict["duration"] = seconds_to_time_string(sum(t.duration for t in tracks))
|
||||||
|
_dict["trackcount"] = len(tracks)
|
||||||
|
|
||||||
|
return _dict
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
item = self.to_full_dict()
|
||||||
|
del item["tracks"]
|
||||||
|
|
||||||
|
return item
|
||||||
@@ -45,6 +45,7 @@ class Track:
|
|||||||
og_title: str = ""
|
og_title: str = ""
|
||||||
artisthashes: list[str] = field(default_factory=list)
|
artisthashes: list[str] = field(default_factory=list)
|
||||||
genrehashes: list[str] = field(default_factory=list)
|
genrehashes: list[str] = field(default_factory=list)
|
||||||
|
weakhash: str = ""
|
||||||
|
|
||||||
_pos: int = 0
|
_pos: int = 0
|
||||||
_ati: str = ""
|
_ati: str = ""
|
||||||
@@ -76,6 +77,7 @@ class Track:
|
|||||||
self.og_title = self.title
|
self.og_title = self.title
|
||||||
self.og_album = self.album
|
self.og_album = self.album
|
||||||
self.folder = self.folder + "/"
|
self.folder = self.folder + "/"
|
||||||
|
self.weakhash = create_hash(self.title, self.artists)
|
||||||
|
|
||||||
self.image = self.albumhash + ".webp"
|
self.image = self.albumhash + ".webp"
|
||||||
self.extra = {
|
self.extra = {
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import json
|
||||||
|
import string
|
||||||
|
import requests
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from app.models.artist import Artist
|
||||||
|
from app.models.mix import Mix
|
||||||
|
from app.models.track import Track
|
||||||
|
from app.plugins import Plugin, plugin_method
|
||||||
|
from app.store.artists import ArtistStore
|
||||||
|
from app.store.tracks import TrackStore
|
||||||
|
from app.utils.dates import get_date_range
|
||||||
|
from app.utils.remove_duplicates import remove_duplicates
|
||||||
|
from app.utils.stats import get_artists_in_period
|
||||||
|
|
||||||
|
|
||||||
|
class MixesPlugin(Plugin):
|
||||||
|
MAX_TRACKS_TO_FETCH = 5
|
||||||
|
TRACK_MIX_LENGTH = 50
|
||||||
|
MIN_TRACK_MIX_LENGTH = 15
|
||||||
|
|
||||||
|
MIN_DAY_LISTEN_DURATION = 3 * 60 # 3 minutes
|
||||||
|
MIN_WEEK_LISTEN_DURATION = 10 * 60 # 10 minutes
|
||||||
|
MIN_MONTH_LISTEN_DURATION = 20 * 60 # 20 minutes
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("mixes", "Mixes")
|
||||||
|
self.server = "https://smcloud.mungaist.com"
|
||||||
|
self.set_active(True)
|
||||||
|
|
||||||
|
@plugin_method
|
||||||
|
def get_track_mix(self, tracks: list[Track], with_help: bool = False):
|
||||||
|
# query = f"{track.title} - {','.join(a['name'] for a in track.artists)}"
|
||||||
|
queries = [
|
||||||
|
{
|
||||||
|
"query": f"{track.title} - {','.join(a['name'] for a in track.artists)}",
|
||||||
|
"album": track.og_album,
|
||||||
|
"with_help": with_help,
|
||||||
|
}
|
||||||
|
for track in tracks
|
||||||
|
]
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server}/radio",
|
||||||
|
json=queries,
|
||||||
|
)
|
||||||
|
results = response.json()
|
||||||
|
|
||||||
|
# artisthashes = results["artists"]
|
||||||
|
trackhashes: list[str] = results["tracks"]
|
||||||
|
|
||||||
|
trackmatches = TrackStore.get_flat_list()
|
||||||
|
trackmatches = [t for t in trackmatches if t.weakhash in trackhashes]
|
||||||
|
|
||||||
|
# filter out duplicates of the same weakhash
|
||||||
|
# group by weakhash and pick the one with the highest bitrate
|
||||||
|
grouped: dict[str, list[Track]] = {}
|
||||||
|
for track in trackmatches:
|
||||||
|
grouped.setdefault(track.weakhash, []).append(track)
|
||||||
|
|
||||||
|
trackmatches = [max(group, key=lambda x: x.bitrate) for group in grouped.values()]
|
||||||
|
|
||||||
|
# sort by trackhash order
|
||||||
|
trackmatches = sorted(trackmatches, key=lambda x: trackhashes.index(x.weakhash))
|
||||||
|
return trackmatches
|
||||||
|
|
||||||
|
@plugin_method
|
||||||
|
def get_artist_mix(self, artisthash: str):
|
||||||
|
artist = ArtistStore.artistmap[artisthash]
|
||||||
|
tracks = TrackStore.get_tracks_by_trackhashes(artist.trackhashes)
|
||||||
|
|
||||||
|
tracks = sorted(tracks, key=lambda x: x.playduration, reverse=True)
|
||||||
|
return self.get_track_mix(tracks[: self.MAX_TRACKS_TO_FETCH])
|
||||||
|
|
||||||
|
@plugin_method
|
||||||
|
def get_artists(self, limit: int = 10):
|
||||||
|
mixes: list[Mix] = []
|
||||||
|
indexed = set()
|
||||||
|
|
||||||
|
today_start, today_end = get_date_range(duration="day")
|
||||||
|
last_2_days_start, last_2_days_end = get_date_range(duration="day", units_ago=2)
|
||||||
|
last_7_days_start, last_7_days_end = get_date_range(duration="week")
|
||||||
|
last_1_month_start, last_1_month_end = get_date_range(duration="month")
|
||||||
|
|
||||||
|
artists = {
|
||||||
|
"today": {
|
||||||
|
"max": 2,
|
||||||
|
"artists": get_artists_in_period(today_start, today_end),
|
||||||
|
"created": 0,
|
||||||
|
},
|
||||||
|
"last_2_days": {
|
||||||
|
"max": 2,
|
||||||
|
"artists": get_artists_in_period(last_2_days_start, last_2_days_end),
|
||||||
|
"created": 0,
|
||||||
|
},
|
||||||
|
"last_7_days": {
|
||||||
|
"max": 3,
|
||||||
|
"artists": get_artists_in_period(last_7_days_start, last_7_days_end),
|
||||||
|
"created": 0,
|
||||||
|
},
|
||||||
|
"last_1_month": {
|
||||||
|
"max": 2,
|
||||||
|
"artists": get_artists_in_period(last_1_month_start, last_1_month_end),
|
||||||
|
"created": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, period in enumerate(artists.values()):
|
||||||
|
# if previous period has less than its max
|
||||||
|
# add the difference to this period's limit
|
||||||
|
limit = period["max"]
|
||||||
|
|
||||||
|
if i > 0:
|
||||||
|
previous_period = artists[list(artists.keys())[i - 1]]
|
||||||
|
if previous_period["created"] < previous_period["max"]:
|
||||||
|
limit += previous_period["max"] - previous_period["created"]
|
||||||
|
|
||||||
|
for artist in period["artists"][:limit]:
|
||||||
|
mix = self.create_artist_mix(artist)
|
||||||
|
|
||||||
|
if mix:
|
||||||
|
mixes.append(mix)
|
||||||
|
indexed.add(artist["artisthash"])
|
||||||
|
period["created"] += 1
|
||||||
|
|
||||||
|
return mixes
|
||||||
|
|
||||||
|
def get_mix_description(self, tracks: list[Track], artishash: str):
|
||||||
|
first_4_artists = []
|
||||||
|
indexed = set()
|
||||||
|
|
||||||
|
for track in tracks:
|
||||||
|
if len(first_4_artists) < 4:
|
||||||
|
if (
|
||||||
|
track.artists[0]["artisthash"] != artishash
|
||||||
|
and track.artists[0]["artisthash"] not in indexed
|
||||||
|
):
|
||||||
|
first_4_artists.append(track.artists[0])
|
||||||
|
indexed.add(track.artists[0]["artisthash"])
|
||||||
|
|
||||||
|
if len(first_4_artists) == 4:
|
||||||
|
return f"Featuring {', '.join(a['name'] for a in first_4_artists)} and more"
|
||||||
|
|
||||||
|
if len(first_4_artists) > 0:
|
||||||
|
return f"Featuring {', '.join(a['name'] for a in first_4_artists)}"
|
||||||
|
|
||||||
|
return f"Featuring {tracks[0].artists[0]['name']}"
|
||||||
|
|
||||||
|
def create_artist_mix(self, artist: dict[str, str]):
|
||||||
|
mix_tracks = self.get_artist_mix(artist["artisthash"])
|
||||||
|
|
||||||
|
if len(mix_tracks) < self.MIN_TRACK_MIX_LENGTH:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return Mix(
|
||||||
|
id=artist["artisthash"],
|
||||||
|
title=artist["artist"],
|
||||||
|
description=self.get_mix_description(mix_tracks, artist["artisthash"]),
|
||||||
|
tracks=[t.trackhash for t in mix_tracks],
|
||||||
|
extra={
|
||||||
|
"type": "artist",
|
||||||
|
"artisthash": artist["artisthash"],
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from app.models.mix import Mix
|
||||||
|
from app.store.tracks import TrackStore
|
||||||
|
from app.utils.auth import get_current_userid
|
||||||
|
|
||||||
|
|
||||||
|
class HomepageStore:
|
||||||
|
entries = {
|
||||||
|
"artist_mixes": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_artist_mixes(cls, mixes: list[Mix], userid: int = 1):
|
||||||
|
idmap = {mix.id: mix for mix in mixes}
|
||||||
|
cls.entries["artist_mixes"][userid] = idmap
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_artist_mixes(cls):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"type": "mix",
|
||||||
|
"item": mix.to_dict(),
|
||||||
|
}
|
||||||
|
for mix in cls.entries["artist_mixes"]
|
||||||
|
.get(get_current_userid(), {})
|
||||||
|
.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_mix(cls, mixtype: str, mixid: str):
|
||||||
|
return cls.entries[mixtype].get(get_current_userid(), {}).get(mixid).to_full_dict()
|
||||||
+10
-3
@@ -66,16 +66,23 @@ def seconds_to_time_string(seconds):
|
|||||||
return f"{remaining_seconds} sec"
|
return f"{remaining_seconds} sec"
|
||||||
|
|
||||||
|
|
||||||
def get_date_range(duration: str):
|
def get_date_range(duration: str, units_ago: int = 0):
|
||||||
"""
|
"""
|
||||||
Returns a tuple of dates representing the start and end of a given duration.
|
Returns a tuple of dates representing the start and end of a given duration.
|
||||||
"""
|
"""
|
||||||
date_range = None
|
date_range = None
|
||||||
|
seconds_ago = (
|
||||||
|
pendulum.now() - pendulum.now().subtract().start_of(duration)
|
||||||
|
).total_seconds() * units_ago
|
||||||
|
print("seconds_ago", duration, str(seconds_ago))
|
||||||
|
|
||||||
match duration:
|
match duration:
|
||||||
case "week" | "month" | "year":
|
case "day" | "week" | "month" | "year":
|
||||||
date_range = (
|
date_range = (
|
||||||
pendulum.now().subtract().start_of(duration).timestamp(),
|
pendulum.now()
|
||||||
|
.subtract(seconds=seconds_ago)
|
||||||
|
.start_of(duration)
|
||||||
|
.timestamp(),
|
||||||
pendulum.now().end_of(duration).timestamp(),
|
pendulum.now().end_of(duration).timestamp(),
|
||||||
)
|
)
|
||||||
case "alltime":
|
case "alltime":
|
||||||
|
|||||||
+4
-2
@@ -13,7 +13,7 @@ from app.utils.dates import seconds_to_time_string
|
|||||||
|
|
||||||
def get_artists_in_period(start_time: int, end_time: int):
|
def get_artists_in_period(start_time: int, end_time: int):
|
||||||
scrobbles = ScrobbleTable.get_all_in_period(start_time, end_time)
|
scrobbles = ScrobbleTable.get_all_in_period(start_time, end_time)
|
||||||
artists = defaultdict(lambda: {"playcount": 0, "playduration": 0})
|
artists: Any = defaultdict(lambda: {"playcount": 0, "playduration": 0})
|
||||||
|
|
||||||
for scrobble in scrobbles:
|
for scrobble in scrobbles:
|
||||||
track = TrackStore.get_tracks_by_trackhashes([scrobble.trackhash])
|
track = TrackStore.get_tracks_by_trackhashes([scrobble.trackhash])
|
||||||
@@ -24,11 +24,13 @@ def get_artists_in_period(start_time: int, end_time: int):
|
|||||||
for artist in track.artists:
|
for artist in track.artists:
|
||||||
artisthash = artist["artisthash"]
|
artisthash = artist["artisthash"]
|
||||||
|
|
||||||
|
artists[artisthash]["artist"] = artist["name"]
|
||||||
artists[artisthash]["artisthash"] = artist["artisthash"]
|
artists[artisthash]["artisthash"] = artist["artisthash"]
|
||||||
artists[artisthash]["playcount"] += 1
|
artists[artisthash]["playcount"] += 1
|
||||||
artists[artisthash]["playduration"] += scrobble.duration
|
artists[artisthash]["playduration"] += scrobble.duration
|
||||||
|
|
||||||
return list(artists.values())
|
artists = list(artists.values())
|
||||||
|
return sorted(artists, key=lambda x: x["playduration"], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
def get_albums_in_period(start_time: int, end_time: int):
|
def get_albums_in_period(start_time: int, end_time: int):
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import setproctitle
|
|||||||
|
|
||||||
from app.api import create_api
|
from app.api import create_api
|
||||||
from app.arg_handler import ProcessArgs
|
from app.arg_handler import ProcessArgs
|
||||||
|
from app.crons import start_cron_jobs
|
||||||
from app.lib.index import IndexEverything
|
from app.lib.index import IndexEverything
|
||||||
from app.plugins.register import register_plugins
|
from app.plugins.register import register_plugins
|
||||||
from app.settings import FLASKVARS, TCOLOR, Info
|
from app.settings import FLASKVARS, TCOLOR, Info
|
||||||
@@ -74,6 +75,7 @@ def run_swingmusic():
|
|||||||
log_startup_info()
|
log_startup_info()
|
||||||
bg_run_setup()
|
bg_run_setup()
|
||||||
register_plugins()
|
register_plugins()
|
||||||
|
start_cron_jobs()
|
||||||
|
|
||||||
# start_watchdog()
|
# start_watchdog()
|
||||||
|
|
||||||
|
|||||||
Generated
+15
-1
@@ -2052,6 +2052,20 @@ files = [
|
|||||||
{file = "roundrobin-0.0.4.tar.gz", hash = "sha256:7e9d19a5bd6123d99993fb935fa86d25c88bb2096e493885f61737ed0f5e9abd"},
|
{file = "roundrobin-0.0.4.tar.gz", hash = "sha256:7e9d19a5bd6123d99993fb935fa86d25c88bb2096e493885f61737ed0f5e9abd"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schedule"
|
||||||
|
version = "1.2.2"
|
||||||
|
description = "Job scheduling for humans."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "schedule-1.2.2-py3-none-any.whl", hash = "sha256:5bef4a2a0183abf44046ae0d164cadcac21b1db011bdd8102e4a0c1e91e06a7d"},
|
||||||
|
{file = "schedule-1.2.2.tar.gz", hash = "sha256:15fe9c75fe5fd9b9627f3f19cc0ef1420508f9f9a46f45cd0769ef75ede5f0b7"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
timezone = ["pytz"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setproctitle"
|
name = "setproctitle"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
@@ -2761,4 +2775,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.10,<3.12"
|
python-versions = ">=3.10,<3.12"
|
||||||
content-hash = "43972b6ffadd14e5047f067a0258f2428ebe351df8bd032dc0bf05df379678a6"
|
content-hash = "85f8932739522e7b53b4fe5bbecc3c10a30bb690e25bf9404209c57ec71e88d3"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ memory-profiler = "^0.61.0"
|
|||||||
sortedcontainers = "^2.4.0"
|
sortedcontainers = "^2.4.0"
|
||||||
xxhash = "^3.4.1"
|
xxhash = "^3.4.1"
|
||||||
ffmpeg-python = "^0.2.0"
|
ffmpeg-python = "^0.2.0"
|
||||||
|
schedule = "^1.2.2"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pylint = "^2.15.5"
|
pylint = "^2.15.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user