mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
new: add system tray icon
This commit is contained in:
@@ -33,6 +33,7 @@ dependencies = [
|
|||||||
"rapidfuzz==3.11.0",
|
"rapidfuzz==3.11.0",
|
||||||
"pendulum>=3.0.0",
|
"pendulum>=3.0.0",
|
||||||
"bjoern>=3.2.2",
|
"bjoern>=3.2.2",
|
||||||
|
"pystray>=0.19.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
+65
-211
@@ -2,34 +2,67 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import click
|
import click
|
||||||
import pathlib
|
import pathlib
|
||||||
import logging
|
import pystray
|
||||||
import mimetypes
|
|
||||||
import setproctitle
|
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
from PIL import Image
|
||||||
|
from typing import Callable
|
||||||
|
from pystray._base import Icon as PystrayIcon
|
||||||
|
|
||||||
|
from swingmusic.start_swingmusic import start_swingmusic
|
||||||
from flask_jwt_extended import (
|
|
||||||
create_access_token,
|
|
||||||
get_jwt,
|
|
||||||
get_jwt_identity,
|
|
||||||
set_access_cookies,
|
|
||||||
verify_jwt_in_request,
|
|
||||||
)
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from flask import Response, request
|
|
||||||
|
|
||||||
from swingmusic import settings
|
|
||||||
from swingmusic.api import create_api
|
|
||||||
from swingmusic.crons import start_cron_jobs
|
|
||||||
from swingmusic.utils.threading import background
|
|
||||||
from swingmusic.setup import load_into_mem, run_setup
|
|
||||||
from swingmusic.plugins.register import register_plugins
|
|
||||||
from swingmusic.utils.xdg_utils import get_xdg_config_dir
|
from swingmusic.utils.xdg_utils import get_xdg_config_dir
|
||||||
from swingmusic.start_info_logger import log_startup_info
|
|
||||||
from swingmusic.utils.filesystem import get_home_res_path
|
from swingmusic.utils.filesystem import get_home_res_path
|
||||||
from swingmusic.utils.paths import getClientFilesExtensions
|
|
||||||
from swingmusic.arg_handler import handle_build, handle_password_reset
|
from swingmusic.arg_handler import handle_build, handle_password_reset
|
||||||
|
|
||||||
|
|
||||||
|
class App:
|
||||||
|
def __init__(self, host: str, port: int, setup: Callable[[], None]):
|
||||||
|
self.host: str = host
|
||||||
|
self.port: int = port
|
||||||
|
self.icon: PystrayIcon = None
|
||||||
|
self.setup = setup
|
||||||
|
self.process = multiprocessing.Process(
|
||||||
|
target=self.setup, args=(self.host, self.port)
|
||||||
|
)
|
||||||
|
|
||||||
|
def start(self, icon: PystrayIcon):
|
||||||
|
self.icon = icon
|
||||||
|
self.icon.visible = True
|
||||||
|
self.process.start()
|
||||||
|
self.icon.run()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
print("\nShutting down ...", end=" ")
|
||||||
|
self.process.terminate()
|
||||||
|
self.process.join(timeout=1)
|
||||||
|
self.icon.stop()
|
||||||
|
print("bye! 👋")
|
||||||
|
|
||||||
|
|
||||||
|
def create_image(width, height, color1, color2):
|
||||||
|
# Generate an image and draw a pattern
|
||||||
|
padding = 7
|
||||||
|
icon_path = get_home_res_path("assets/logo-fill.light.ico")
|
||||||
|
image = Image.open(icon_path)
|
||||||
|
|
||||||
|
# Calculate new size with padding
|
||||||
|
new_size = (width - 2 * padding, height - 2 * padding)
|
||||||
|
|
||||||
|
# Resize the image while maintaining aspect ratio
|
||||||
|
image.thumbnail(new_size, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
# Create a new image with padding
|
||||||
|
padded_image = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||||
|
|
||||||
|
# Calculate position to center the image
|
||||||
|
x = (width - image.width) // 2
|
||||||
|
y = (height - image.height) // 2
|
||||||
|
|
||||||
|
# Paste the resized image onto the padded image
|
||||||
|
padded_image.paste(image, (x, y), image)
|
||||||
|
|
||||||
|
return padded_image
|
||||||
|
|
||||||
|
|
||||||
def print_version(*args, **kwargs):
|
def print_version(*args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Prints the version of the application.
|
Prints the version of the application.
|
||||||
@@ -49,194 +82,6 @@ def print_version(*args, **kwargs):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
def start_swingmusic(host: str, port: int):
|
|
||||||
"""
|
|
||||||
Creates and starts the Flask application server for Swing Music.
|
|
||||||
|
|
||||||
This function sets up the Flask application with all necessary
|
|
||||||
configurations, including static file handling, authentication middleware, and
|
|
||||||
server setup, then runs it. It also sets up background tasks and cron jobs.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
host (str): The host address to bind the server to (e.g., 'localhost' or '0.0.0.0')
|
|
||||||
port (int): The port number to run the server on
|
|
||||||
|
|
||||||
Note:
|
|
||||||
The application uses either bjoern or waitress as the WSGI server,
|
|
||||||
depending on availability. It also includes JWT authentication,
|
|
||||||
static file serving with gzip compression support, and automatic
|
|
||||||
token refresh functionality.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Example: Setting up dirs, database, and loading stuff into memory.
|
|
||||||
# TIP: Be careful with the order of the setup functions.
|
|
||||||
|
|
||||||
# Load mimetypes for the web client's static files
|
|
||||||
# Loading mimetypes should happen automatically but
|
|
||||||
# sometimes the mimetypes are not loaded correctly
|
|
||||||
# eg. when the Registry is messed up on Windows.
|
|
||||||
|
|
||||||
# See the following issues:
|
|
||||||
# https://github.com/swingmx/swingmusic/issues/137
|
|
||||||
|
|
||||||
mimetypes.add_type("text/css", ".css")
|
|
||||||
mimetypes.add_type("text/javascript", ".js")
|
|
||||||
mimetypes.add_type("text/plain", ".txt")
|
|
||||||
mimetypes.add_type("text/html", ".html")
|
|
||||||
mimetypes.add_type("image/webp", ".webp")
|
|
||||||
mimetypes.add_type("image/svg+xml", ".svg")
|
|
||||||
mimetypes.add_type("image/png", ".png")
|
|
||||||
mimetypes.add_type("image/vnd.microsoft.icon", ".ico")
|
|
||||||
mimetypes.add_type("image/gif", ".gif")
|
|
||||||
mimetypes.add_type("font/woff", ".woff")
|
|
||||||
mimetypes.add_type("application/manifest+json", ".webmanifest")
|
|
||||||
|
|
||||||
# logging.disable(logging.CRITICAL)
|
|
||||||
# werkzeug = logging.getLogger("werkzeug")
|
|
||||||
# werkzeug.setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
waitress_logger = logging.getLogger("waitress")
|
|
||||||
waitress_logger.setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
log_startup_info(host, port)
|
|
||||||
|
|
||||||
@background
|
|
||||||
def run_swingmusic():
|
|
||||||
register_plugins()
|
|
||||||
|
|
||||||
setproctitle.setproctitle(f"swingmusic {host}:{port}")
|
|
||||||
start_cron_jobs()
|
|
||||||
|
|
||||||
# Setup function calls
|
|
||||||
settings.Info.load()
|
|
||||||
run_setup()
|
|
||||||
|
|
||||||
# Create the Flask app
|
|
||||||
app = create_api()
|
|
||||||
app.static_folder = get_home_res_path("client")
|
|
||||||
|
|
||||||
# INFO: Routes that don't need authentication
|
|
||||||
whitelisted_routes = {
|
|
||||||
"/auth/login",
|
|
||||||
"/auth/users",
|
|
||||||
"/auth/pair",
|
|
||||||
"/auth/logout",
|
|
||||||
"/auth/refresh",
|
|
||||||
"/docs",
|
|
||||||
}
|
|
||||||
blacklist_extensions = {".webp", ".jpg"}.union(getClientFilesExtensions())
|
|
||||||
|
|
||||||
def skipAuthAction():
|
|
||||||
"""
|
|
||||||
Skips the JWT verification for the current request.
|
|
||||||
"""
|
|
||||||
if request.path == "/" or any(
|
|
||||||
request.path.endswith(ext) for ext in blacklist_extensions
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# if request path starts with any of the blacklisted routes, don't verify jwt
|
|
||||||
if any(request.path.startswith(route) for route in whitelisted_routes):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def verify_auth():
|
|
||||||
"""
|
|
||||||
Verifies the JWT token before each request.
|
|
||||||
"""
|
|
||||||
if skipAuthAction():
|
|
||||||
return
|
|
||||||
|
|
||||||
verify_jwt_in_request()
|
|
||||||
|
|
||||||
@app.after_request
|
|
||||||
def refresh_expiring_jwt(response: Response):
|
|
||||||
"""
|
|
||||||
Refreshes the cookies JWT token after each request.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# INFO: If the request has an Authorization header, don't refresh the jwt
|
|
||||||
# Request is probably from the mobile client or a third party
|
|
||||||
if skipAuthAction() or request.headers.get("Authorization"):
|
|
||||||
return response
|
|
||||||
|
|
||||||
try:
|
|
||||||
exp_timestamp = get_jwt()["exp"]
|
|
||||||
now = datetime.now(timezone.utc)
|
|
||||||
target_timestamp = datetime.timestamp(now) + 60 * 60 * 24 * 7 # 7 days
|
|
||||||
|
|
||||||
if target_timestamp > exp_timestamp:
|
|
||||||
access_token = create_access_token(identity=get_jwt_identity())
|
|
||||||
set_access_cookies(response, access_token)
|
|
||||||
|
|
||||||
return response
|
|
||||||
except (RuntimeError, KeyError):
|
|
||||||
return response
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
|
||||||
def serve_client_files(path: str):
|
|
||||||
"""
|
|
||||||
Serves the static files in the client folder.
|
|
||||||
"""
|
|
||||||
js_or_css = path.endswith(".js") or path.endswith(".css")
|
|
||||||
if not js_or_css:
|
|
||||||
return app.send_static_file(path)
|
|
||||||
|
|
||||||
gzipped_path = path + ".gz"
|
|
||||||
user_agent = request.headers.get("User-Agent")
|
|
||||||
|
|
||||||
# INFO: Safari doesn't support gzip encoding
|
|
||||||
# See issue: https://github.com/swingmx/swingmusic/issues/155
|
|
||||||
is_safari = (
|
|
||||||
user_agent
|
|
||||||
and user_agent.find("Safari") >= 0
|
|
||||||
and user_agent.find("Chrome") < 0
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_safari:
|
|
||||||
return app.send_static_file(path)
|
|
||||||
|
|
||||||
accepts_gzip = request.headers.get("Accept-Encoding", "").find("gzip") >= 0
|
|
||||||
|
|
||||||
if accepts_gzip:
|
|
||||||
if os.path.exists(os.path.join(app.static_folder or "", gzipped_path)):
|
|
||||||
response = app.make_response(app.send_static_file(gzipped_path))
|
|
||||||
response.headers["Content-Encoding"] = "gzip"
|
|
||||||
return response
|
|
||||||
|
|
||||||
return app.send_static_file(path)
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def serve_client():
|
|
||||||
"""
|
|
||||||
Serves the index.html file at `client/index.html`.
|
|
||||||
"""
|
|
||||||
return app.send_static_file("index.html")
|
|
||||||
|
|
||||||
load_into_mem()
|
|
||||||
run_swingmusic()
|
|
||||||
# TrackStore.export()
|
|
||||||
# ArtistStore.export()
|
|
||||||
|
|
||||||
try:
|
|
||||||
import bjoern
|
|
||||||
|
|
||||||
bjoern.run(app, host, port)
|
|
||||||
except ImportError:
|
|
||||||
import waitress
|
|
||||||
|
|
||||||
waitress.serve(
|
|
||||||
app,
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
threads=100,
|
|
||||||
ipv6=True,
|
|
||||||
ipv4=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@click.command(options_metavar="<options>", context_settings={"show_default": True})
|
@click.command(options_metavar="<options>", context_settings={"show_default": True})
|
||||||
@click.option(
|
@click.option(
|
||||||
"--build",
|
"--build",
|
||||||
@@ -287,7 +132,16 @@ def run(*args, **kwargs):
|
|||||||
os.environ["SWINGMUSIC_XDG_CONFIG_DIR"] = str(
|
os.environ["SWINGMUSIC_XDG_CONFIG_DIR"] = str(
|
||||||
pathlib.Path(kwargs["config"]).resolve()
|
pathlib.Path(kwargs["config"]).resolve()
|
||||||
)
|
)
|
||||||
start_swingmusic(kwargs["host"], kwargs["port"])
|
|
||||||
|
app = App(kwargs["host"], kwargs["port"], start_swingmusic)
|
||||||
|
icon = pystray.Icon(
|
||||||
|
"Swing Music",
|
||||||
|
icon=create_image(64, 64, "black", "white"),
|
||||||
|
menu=pystray.Menu(
|
||||||
|
pystray.MenuItem("Quit Swing Music", app.stop),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
app.start(icon)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
from swingmusic import settings
|
||||||
|
from swingmusic.api import create_api
|
||||||
|
from swingmusic.crons import start_cron_jobs
|
||||||
|
from swingmusic.plugins.register import register_plugins
|
||||||
|
from swingmusic.setup import load_into_mem, run_setup
|
||||||
|
from swingmusic.start_info_logger import log_startup_info
|
||||||
|
from swingmusic.utils.filesystem import get_home_res_path
|
||||||
|
from swingmusic.utils.paths import getClientFilesExtensions
|
||||||
|
from swingmusic.utils.threading import background
|
||||||
|
|
||||||
|
|
||||||
|
import setproctitle
|
||||||
|
from flask import Response, request
|
||||||
|
from flask_jwt_extended import create_access_token, get_jwt, get_jwt_identity, set_access_cookies, verify_jwt_in_request
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
def start_swingmusic(host: str, port: int):
|
||||||
|
"""
|
||||||
|
Creates and starts the Flask application server for Swing Music.
|
||||||
|
|
||||||
|
This function sets up the Flask application with all necessary
|
||||||
|
configurations, including static file handling, authentication middleware, and
|
||||||
|
server setup, then runs it. It also sets up background tasks and cron jobs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host (str): The host address to bind the server to (e.g., 'localhost' or '0.0.0.0')
|
||||||
|
port (int): The port number to run the server on
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The application uses either bjoern or waitress as the WSGI server,
|
||||||
|
depending on availability. It also includes JWT authentication,
|
||||||
|
static file serving with gzip compression support, and automatic
|
||||||
|
token refresh functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Example: Setting up dirs, database, and loading stuff into memory.
|
||||||
|
# TIP: Be careful with the order of the setup functions.
|
||||||
|
|
||||||
|
# Load mimetypes for the web client's static files
|
||||||
|
# Loading mimetypes should happen automatically but
|
||||||
|
# sometimes the mimetypes are not loaded correctly
|
||||||
|
# eg. when the Registry is messed up on Windows.
|
||||||
|
|
||||||
|
# See the following issues:
|
||||||
|
# https://github.com/swingmx/swingmusic/issues/137
|
||||||
|
|
||||||
|
mimetypes.add_type("text/css", ".css")
|
||||||
|
mimetypes.add_type("text/javascript", ".js")
|
||||||
|
mimetypes.add_type("text/plain", ".txt")
|
||||||
|
mimetypes.add_type("text/html", ".html")
|
||||||
|
mimetypes.add_type("image/webp", ".webp")
|
||||||
|
mimetypes.add_type("image/svg+xml", ".svg")
|
||||||
|
mimetypes.add_type("image/png", ".png")
|
||||||
|
mimetypes.add_type("image/vnd.microsoft.icon", ".ico")
|
||||||
|
mimetypes.add_type("image/gif", ".gif")
|
||||||
|
mimetypes.add_type("font/woff", ".woff")
|
||||||
|
mimetypes.add_type("application/manifest+json", ".webmanifest")
|
||||||
|
|
||||||
|
# logging.disable(logging.CRITICAL)
|
||||||
|
# werkzeug = logging.getLogger("werkzeug")
|
||||||
|
# werkzeug.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
waitress_logger = logging.getLogger("waitress")
|
||||||
|
waitress_logger.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
log_startup_info(host, port)
|
||||||
|
|
||||||
|
@background
|
||||||
|
def run_swingmusic():
|
||||||
|
register_plugins()
|
||||||
|
|
||||||
|
setproctitle.setproctitle(f"swingmusic {host}:{port}")
|
||||||
|
start_cron_jobs()
|
||||||
|
|
||||||
|
# Setup function calls
|
||||||
|
settings.Info.load()
|
||||||
|
run_setup()
|
||||||
|
|
||||||
|
# Create the Flask app
|
||||||
|
app = create_api()
|
||||||
|
app.static_folder = get_home_res_path("client")
|
||||||
|
|
||||||
|
# INFO: Routes that don't need authentication
|
||||||
|
whitelisted_routes = {
|
||||||
|
"/auth/login",
|
||||||
|
"/auth/users",
|
||||||
|
"/auth/pair",
|
||||||
|
"/auth/logout",
|
||||||
|
"/auth/refresh",
|
||||||
|
"/docs",
|
||||||
|
}
|
||||||
|
blacklist_extensions = {".webp", ".jpg"}.union(getClientFilesExtensions())
|
||||||
|
|
||||||
|
def skipAuthAction():
|
||||||
|
"""
|
||||||
|
Skips the JWT verification for the current request.
|
||||||
|
"""
|
||||||
|
if request.path == "/" or any(
|
||||||
|
request.path.endswith(ext) for ext in blacklist_extensions
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# if request path starts with any of the blacklisted routes, don't verify jwt
|
||||||
|
if any(request.path.startswith(route) for route in whitelisted_routes):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def verify_auth():
|
||||||
|
"""
|
||||||
|
Verifies the JWT token before each request.
|
||||||
|
"""
|
||||||
|
if skipAuthAction():
|
||||||
|
return
|
||||||
|
|
||||||
|
verify_jwt_in_request()
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def refresh_expiring_jwt(response: Response):
|
||||||
|
"""
|
||||||
|
Refreshes the cookies JWT token after each request.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# INFO: If the request has an Authorization header, don't refresh the jwt
|
||||||
|
# Request is probably from the mobile client or a third party
|
||||||
|
if skipAuthAction() or request.headers.get("Authorization"):
|
||||||
|
return response
|
||||||
|
|
||||||
|
try:
|
||||||
|
exp_timestamp = get_jwt()["exp"]
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
target_timestamp = datetime.timestamp(now) + 60 * 60 * 24 * 7 # 7 days
|
||||||
|
|
||||||
|
if target_timestamp > exp_timestamp:
|
||||||
|
access_token = create_access_token(identity=get_jwt_identity())
|
||||||
|
set_access_cookies(response, access_token)
|
||||||
|
|
||||||
|
return response
|
||||||
|
except (RuntimeError, KeyError):
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.route("/<path:path>")
|
||||||
|
def serve_client_files(path: str):
|
||||||
|
"""
|
||||||
|
Serves the static files in the client folder.
|
||||||
|
"""
|
||||||
|
js_or_css = path.endswith(".js") or path.endswith(".css")
|
||||||
|
if not js_or_css:
|
||||||
|
return app.send_static_file(path)
|
||||||
|
|
||||||
|
gzipped_path = path + ".gz"
|
||||||
|
user_agent = request.headers.get("User-Agent")
|
||||||
|
|
||||||
|
# INFO: Safari doesn't support gzip encoding
|
||||||
|
# See issue: https://github.com/swingmx/swingmusic/issues/155
|
||||||
|
is_safari = (
|
||||||
|
user_agent
|
||||||
|
and user_agent.find("Safari") >= 0
|
||||||
|
and user_agent.find("Chrome") < 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_safari:
|
||||||
|
return app.send_static_file(path)
|
||||||
|
|
||||||
|
accepts_gzip = request.headers.get("Accept-Encoding", "").find("gzip") >= 0
|
||||||
|
|
||||||
|
if accepts_gzip:
|
||||||
|
if os.path.exists(os.path.join(app.static_folder or "", gzipped_path)):
|
||||||
|
response = app.make_response(app.send_static_file(gzipped_path))
|
||||||
|
response.headers["Content-Encoding"] = "gzip"
|
||||||
|
return response
|
||||||
|
|
||||||
|
return app.send_static_file(path)
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def serve_client():
|
||||||
|
"""
|
||||||
|
Serves the index.html file at `client/index.html`.
|
||||||
|
"""
|
||||||
|
return app.send_static_file("index.html")
|
||||||
|
|
||||||
|
load_into_mem()
|
||||||
|
run_swingmusic()
|
||||||
|
# TrackStore.export()
|
||||||
|
# ArtistStore.export()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import bjoern
|
||||||
|
|
||||||
|
bjoern.run(app, host, port)
|
||||||
|
except ImportError:
|
||||||
|
import waitress
|
||||||
|
|
||||||
|
waitress.serve(
|
||||||
|
app,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
threads=100,
|
||||||
|
ipv6=True,
|
||||||
|
ipv4=True,
|
||||||
|
)
|
||||||
@@ -685,6 +685,54 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 },
|
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyobjc-core"
|
||||||
|
version = "11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5c/94/a111239b98260869780a5767e5d74bfd3a8c13a40457f479c28dcd91f89d/pyobjc_core-11.0.tar.gz", hash = "sha256:63bced211cb8a8fb5c8ff46473603da30e51112861bd02c438fbbbc8578d9a70", size = 994931 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/05/fa97309c3b1bc1ec90d701db89902e0bd5e1024023aa2c5387b889458b1b/pyobjc_core-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:50675c0bb8696fe960a28466f9baf6943df2928a1fd85625d678fa2f428bd0bd", size = 727295 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyobjc-framework-cocoa"
|
||||||
|
version = "11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyobjc-core" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c5/32/53809096ad5fc3e7a2c5ddea642590a5f2cb5b81d0ad6ea67fdb2263d9f9/pyobjc_framework_cocoa-11.0.tar.gz", hash = "sha256:00346a8cb81ad7b017b32ff7bf596000f9faa905807b1bd234644ebd47f692c5", size = 6173848 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/97/81fd41ad90e9c241172110aa635a6239d56f50d75923aaedbbe351828580/pyobjc_framework_Cocoa-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3ea7be6e6dd801b297440de02d312ba3fa7fd3c322db747ae1cb237e975f5d33", size = 385534 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyobjc-framework-quartz"
|
||||||
|
version = "11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyobjc-core" },
|
||||||
|
{ name = "pyobjc-framework-cocoa" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a5/ad/f00f3f53387c23bbf4e0bb1410e11978cbf87c82fa6baff0ee86f74c5fb6/pyobjc_framework_quartz-11.0.tar.gz", hash = "sha256:3205bf7795fb9ae34747f701486b3db6dfac71924894d1f372977c4d70c3c619", size = 3952463 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/6a/68957c8c5e8f0128d4d419728bac397d48fa7ad7a66e82b70e64d129ffca/pyobjc_framework_Quartz-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d251696bfd8e8ef72fbc90eb29fec95cb9d1cc409008a183d5cc3246130ae8c2", size = 212349 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pystray"
|
||||||
|
version = "0.19.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" },
|
||||||
|
{ name = "python-xlib", marker = "sys_platform == 'linux'" },
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/64/927a4b9024196a4799eba0180e0ca31568426f258a4a5c90f87a97f51d28/pystray-0.19.5-py2.py3-none-any.whl", hash = "sha256:a0c2229d02cf87207297c22d86ffc57c86c227517b038c0d3c59df79295ac617", size = 49068 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
@@ -728,6 +776,18 @@ client = [
|
|||||||
{ name = "websocket-client" },
|
{ name = "websocket-client" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-xlib"
|
||||||
|
version = "0.33"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pywin32"
|
name = "pywin32"
|
||||||
version = "308"
|
version = "308"
|
||||||
@@ -942,6 +1002,7 @@ dependencies = [
|
|||||||
{ name = "pendulum" },
|
{ name = "pendulum" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "psutil" },
|
{ name = "psutil" },
|
||||||
|
{ name = "pystray" },
|
||||||
{ name = "rapidfuzz" },
|
{ name = "rapidfuzz" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "schedule" },
|
{ name = "schedule" },
|
||||||
@@ -977,6 +1038,7 @@ requires-dist = [
|
|||||||
{ name = "pendulum", specifier = ">=3.0.0" },
|
{ name = "pendulum", specifier = ">=3.0.0" },
|
||||||
{ name = "pillow", specifier = ">=11.1.0" },
|
{ name = "pillow", specifier = ">=11.1.0" },
|
||||||
{ name = "psutil", specifier = ">=5.9.4" },
|
{ name = "psutil", specifier = ">=5.9.4" },
|
||||||
|
{ name = "pystray", specifier = ">=0.19.5" },
|
||||||
{ name = "rapidfuzz", specifier = "==3.11.0" },
|
{ name = "rapidfuzz", specifier = "==3.11.0" },
|
||||||
{ name = "requests", specifier = ">=2.27.1" },
|
{ name = "requests", specifier = ">=2.27.1" },
|
||||||
{ name = "schedule", specifier = ">=1.2.2" },
|
{ name = "schedule", specifier = ">=1.2.2" },
|
||||||
|
|||||||
Reference in New Issue
Block a user