Merge branch 'main' of github.com:jroeger23/.hive

This commit is contained in:
Jonas Röger 2025-08-21 17:14:38 +02:00
commit d121312dbd
Signed by: jonas
GPG Key ID: 4000EB35E1AE0F07
29 changed files with 532 additions and 82 deletions

132
flake.lock generated
View File

@ -20,11 +20,11 @@
]
},
"locked": {
"lastModified": 1750372185,
"narHash": "sha256-lVBKxd9dsZOH1fA6kSE5WNnt8e+09fN+NL/Q3BjTWHY=",
"lastModified": 1753216019,
"narHash": "sha256-zik7WISrR1ks2l6T1MZqZHb/OqroHdJnSnAehkE0kCk=",
"owner": "hyprwm",
"repo": "aquamarine",
"rev": "7cef49d261cbbe537e8cb662485e76d29ac4cbca",
"rev": "be166e11d86ba4186db93e10c54a141058bdce49",
"type": "github"
},
"original": {
@ -61,11 +61,11 @@
},
"locked": {
"dir": "pkgs/firefox-addons",
"lastModified": 1750737804,
"narHash": "sha256-wClGd2PhxdjjphR6wIgoiDcR+Gfg4/+FyseSOjIIzVU=",
"lastModified": 1754512310,
"narHash": "sha256-gXE5lTYMOhpDJo+siLXW/3BzySPmLMD12GVB1QFVbyw=",
"owner": "rycee",
"repo": "nur-expressions",
"rev": "aaaf4fec792bad465ea4a35c0be5bc2a54f33095",
"rev": "2008f9aa7a5ccde48bfc1de5a919be5898da09c2",
"type": "gitlab"
},
"original": {
@ -78,11 +78,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
@ -136,11 +136,11 @@
]
},
"locked": {
"lastModified": 1750783375,
"narHash": "sha256-oKccVOF1igIwTncVTHZ9RHgjOQEMbg8NK5am2IjOCCI=",
"lastModified": 1753592768,
"narHash": "sha256-oV695RvbAE4+R9pcsT9shmp6zE/+IZe6evHWX63f2Qg=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "d457fa3c764e53e7bdd7354467c605766407620d",
"rev": "fc3add429f21450359369af74c2375cb34a2d204",
"type": "github"
},
"original": {
@ -166,11 +166,11 @@
]
},
"locked": {
"lastModified": 1749155331,
"narHash": "sha256-XR9fsI0zwLiFWfqi/pdS/VD+YNorKb3XIykgTg4l1nA=",
"lastModified": 1753964049,
"narHash": "sha256-lIqabfBY7z/OANxHoPeIrDJrFyYy9jAM4GQLzZ2feCM=",
"owner": "hyprwm",
"repo": "hyprcursor",
"rev": "45fcc10b4c282746d93ec406a740c43b48b4ef80",
"rev": "44e91d467bdad8dcf8bbd2ac7cf49972540980a5",
"type": "github"
},
"original": {
@ -195,11 +195,11 @@
]
},
"locked": {
"lastModified": 1750371717,
"narHash": "sha256-cNP+bVq8m5x2Rl6MTjwfQLCdwbVmKvTH7yqVc1SpiJM=",
"lastModified": 1754305013,
"narHash": "sha256-u+M2f0Xf1lVHzIPQ7DsNCDkM1NYxykOSsRr4t3TbSM4=",
"owner": "hyprwm",
"repo": "hyprgraphics",
"rev": "15c6f8f3a567fec9a0f732cd310a7ff456deef88",
"rev": "4c1d63a0f22135db123fc789f174b89544c6ec2d",
"type": "github"
},
"original": {
@ -226,11 +226,11 @@
"xdph": "xdph"
},
"locked": {
"lastModified": 1750771433,
"narHash": "sha256-AG2TRRcc84066tAOdJ1hdy1ZbpR53UbqGxmaL3VecRc=",
"lastModified": 1754844749,
"narHash": "sha256-QNT0yXHyjvZ++vrJICAWFBMrcrTVbgRIZLplmOv1W7s=",
"owner": "hyprwm",
"repo": "Hyprland",
"rev": "aea81320015130bf850242d5a8695fcdcbf4f0c1",
"rev": "584b844aaf72cd7ea6851117f1bd598b7467ffc1",
"type": "github"
},
"original": {
@ -256,11 +256,11 @@
]
},
"locked": {
"lastModified": 1750778528,
"narHash": "sha256-X0QpVEhpkhf0RvU0n5+qsBH3JIXY2uZ8m56HhP7FzU8=",
"lastModified": 1754766309,
"narHash": "sha256-pANfQZ22RNF6sCFxrMahjE70v/HbGfA4lPZ7pTmfwUQ=",
"owner": "hyprwm",
"repo": "hyprland-plugins",
"rev": "aa23323de3325e3026fc26f9c23205954be4d337",
"rev": "833af8e8c6f035a53a167aff59e5e85bf0386d93",
"type": "github"
},
"original": {
@ -349,11 +349,11 @@
]
},
"locked": {
"lastModified": 1750371812,
"narHash": "sha256-D868K1dVEACw17elVxRgXC6hOxY+54wIEjURztDWLk8=",
"lastModified": 1753819801,
"narHash": "sha256-tHe6XeNeVeKapkNM3tcjW4RuD+tB2iwwoogWJOtsqTI=",
"owner": "hyprwm",
"repo": "hyprland-qtutils",
"rev": "b13c7481e37856f322177010bdf75fccacd1adc8",
"rev": "b308a818b9dcaa7ab8ccab891c1b84ebde2152bc",
"type": "github"
},
"original": {
@ -378,11 +378,11 @@
]
},
"locked": {
"lastModified": 1750371198,
"narHash": "sha256-/iuJ1paQOBoSLqHflRNNGyroqfF/yvPNurxzcCT0cAE=",
"lastModified": 1753622892,
"narHash": "sha256-0K+A+gmOI8IklSg5It1nyRNv0kCNL51duwnhUO/B8JA=",
"owner": "hyprwm",
"repo": "hyprlang",
"rev": "cee01452bca58d6cadb3224e21e370de8bc20f0b",
"rev": "23f0debd2003f17bd65f851cd3f930cff8a8c809",
"type": "github"
},
"original": {
@ -403,11 +403,11 @@
]
},
"locked": {
"lastModified": 1750371096,
"narHash": "sha256-JB1IeJ41y7kWc/dPGV6RMcCUM0Xj2NEK26A2Ap7EM9c=",
"lastModified": 1754481650,
"narHash": "sha256-6u6HdEFJh5gY6VfyMQbhP7zDdVcqOrCDTkbiHJmAtMI=",
"owner": "hyprwm",
"repo": "hyprutils",
"rev": "38f3a211657ce82a1123bf19402199b67a410f08",
"rev": "df6b8820c4a0835d83d0c7c7be86fbc555f1f7fd",
"type": "github"
},
"original": {
@ -428,11 +428,11 @@
]
},
"locked": {
"lastModified": 1750371869,
"narHash": "sha256-lGk4gLjgZQ/rndUkzmPYcgbHr8gKU5u71vyrjnwfpB4=",
"lastModified": 1751897909,
"narHash": "sha256-FnhBENxihITZldThvbO7883PdXC/2dzW4eiNvtoV5Ao=",
"owner": "hyprwm",
"repo": "hyprwayland-scanner",
"rev": "aa38edd6e3e277ae6a97ea83a69261a5c3aab9fd",
"rev": "fcca0c61f988a9d092cbb33e906775014c61579d",
"type": "github"
},
"original": {
@ -451,11 +451,11 @@
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1750763263,
"narHash": "sha256-3JW7xEfobw0qXZsOZ0BwGV5+JMzOE+fZ3+v0ypOwKt0=",
"lastModified": 1751591814,
"narHash": "sha256-A4lgvuj4v+Pr8MniXz1FBG0DXOygi8tTECR+j53FMhM=",
"owner": "lilyinstarlight",
"repo": "nixos-cosmic",
"rev": "dabf86334f0eab8ced9a5e7219d34a28b055bb2a",
"rev": "fef2d0c78c4e4d6c600a88795af193131ff51bdc",
"type": "github"
},
"original": {
@ -466,11 +466,11 @@
},
"nixos-hardware": {
"locked": {
"lastModified": 1750431636,
"narHash": "sha256-vnzzBDbCGvInmfn2ijC4HsIY/3W1CWbwS/YQoFgdgPg=",
"lastModified": 1754564048,
"narHash": "sha256-dz303vGuzWjzOPOaYkS9xSW+B93PSAJxvBd6CambXVA=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "1552a9f4513f3f0ceedcf90320e48d3d47165712",
"rev": "26ed7a0d4b8741fe1ef1ee6fa64453ca056ce113",
"type": "github"
},
"original": {
@ -482,11 +482,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1750622754,
"narHash": "sha256-kMhs+YzV4vPGfuTpD3mwzibWUE6jotw5Al2wczI0Pv8=",
"lastModified": 1754767907,
"narHash": "sha256-8OnUzRQZkqtUol9vuUuQC30hzpMreKptNyET2T9lB6g=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c7ab75210cb8cb16ddd8f290755d9558edde7ee1",
"rev": "c5f08b62ed75415439d48152c2a784e36909b1bc",
"type": "github"
},
"original": {
@ -498,11 +498,11 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1750646418,
"narHash": "sha256-4UAN+W0Lp4xnUiHYXUXAPX18t+bn6c4Btry2RqM9JHY=",
"lastModified": 1751048012,
"narHash": "sha256-MYbotu4UjWpTsq01wglhN5xDRfZYLFtNk7SBY0BcjkU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1f426f65ac4e6bf808923eb6f8b8c2bfba3d18c5",
"rev": "a684c58d46ebbede49f280b653b9e56100aa3877",
"type": "github"
},
"original": {
@ -514,11 +514,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1750506804,
"narHash": "sha256-VLFNc4egNjovYVxDGyBYTrvVCgDYgENp5bVi9fPTDYc=",
"lastModified": 1754725699,
"narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4206c4cb56751df534751b058295ea61357bbbaa",
"rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054",
"type": "github"
},
"original": {
@ -538,11 +538,11 @@
]
},
"locked": {
"lastModified": 1748196248,
"narHash": "sha256-1iHjsH6/5UOerJEoZKE+Gx1BgAoge/YcnUsOA4wQ/BU=",
"lastModified": 1754501628,
"narHash": "sha256-FExJ54tVB5iu7Dh2tLcyCSWpaV+lmUzzWKZUkemwXvo=",
"owner": "pjones",
"repo": "plasma-manager",
"rev": "b7697abe89967839b273a863a3805345ea54ab56",
"rev": "cca090f8115c4172b9aef6c5299ae784bdd5e133",
"type": "github"
},
"original": {
@ -561,11 +561,11 @@
]
},
"locked": {
"lastModified": 1749636823,
"narHash": "sha256-WUaIlOlPLyPgz9be7fqWJA5iG6rHcGRtLERSCfUDne4=",
"lastModified": 1754416808,
"narHash": "sha256-c6yg0EQ9xVESx6HGDOCMcyRSjaTpNJP10ef+6fRcofA=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "623c56286de5a3193aa38891a6991b28f9bab056",
"rev": "9c52372878df6911f9afc1e2a1391f55e4dfc864",
"type": "github"
},
"original": {
@ -597,11 +597,11 @@
]
},
"locked": {
"lastModified": 1750732748,
"narHash": "sha256-HR2b3RHsPeJm+Fb+1ui8nXibgniVj7hBNvUbXEyz0DU=",
"lastModified": 1751251399,
"narHash": "sha256-y+viCuy/eKKpkX1K2gDvXIJI/yzvy6zA3HObapz9XZ0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "4b4494b2ba7e8a8041b2e28320b2ee02c115c75f",
"rev": "b22d5ee8c60ed1291521f2dde48784edd6bf695b",
"type": "github"
},
"original": {
@ -617,11 +617,11 @@
]
},
"locked": {
"lastModified": 1750119275,
"narHash": "sha256-Rr7Pooz9zQbhdVxux16h7URa6mA80Pb/G07T4lHvh0M=",
"lastModified": 1754328224,
"narHash": "sha256-glPK8DF329/dXtosV7YSzRlF4n35WDjaVwdOMEoEXHA=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "77c423a03b9b2b79709ea2cb63336312e78b72e2",
"rev": "49021900e69812ba7ddb9e40f9170218a7eca9f4",
"type": "github"
},
"original": {
@ -673,11 +673,11 @@
]
},
"locked": {
"lastModified": 1750372504,
"narHash": "sha256-VBeZb1oqZM1cqCAZnFz/WyYhO8aF/ImagI7WWg/Z3Og=",
"lastModified": 1753633878,
"narHash": "sha256-js2sLRtsOUA/aT10OCDaTjO80yplqwOIaLUqEe0nMx0=",
"owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland",
"rev": "400308fc4f9d12e0a93e483c2e7a649e12af1a92",
"rev": "371b96bd11ad2006ed4f21229dbd1be69bed3e8a",
"type": "github"
},
"original": {

View File

@ -209,6 +209,7 @@
devShells.${system} = {
transcode-davinci-resolve = (import ./pkgs/transcode-davinci-resolve/shell.nix) {pkgs = nixpkgs.legacyPackages.${system};};
spotify-shortcuts = (import ./pkgs/spotify-shortcuts/shell.nix) {pkgs = nixpkgs.legacyPackages.${system};};
};
overlays.default = import ./pkgs;

View File

@ -16,10 +16,6 @@
hive.doom.withNixPkgs = true;
hive.doom.withShellPkgs = true;
hive.doom.withCXXPkgs = true;
hive.programs.creative = {
enable = true;
video-editing-light = true;
};
# This value determines the Home Manager release that your configuration is
# compatible with. This helps avoid breakage when a new Home Manager release

View File

@ -3,7 +3,6 @@
# and in the NixOS manual (accessible by running nixos-help).
{
config,
inputs,
pkgs,
...
}: {
@ -18,6 +17,14 @@
sopsFile = ../../secrets/monolith/wg.yaml;
key = "privateKey";
};
sops.secrets.spotifyShortcutsClientId = {
sopsFile = ../../secrets/spotify-shortcuts.yaml;
key = "clientId";
};
sops.secrets.spotifyShortcutsClientSecret = {
sopsFile = ../../secrets/spotify-shortcuts.yaml;
key = "clientSecret";
};
# Users
users.users.jonas = {
@ -44,6 +51,7 @@
# hive modules
hive.nix-scripts.enable = true;
hive.displayManager.name = "sddm";
hive.themes.layan.enable = true;
hive.plasma.enable = true;
hive.kwallet.enable = true;
hive.kwallet.forUsers = ["jonas"];
@ -70,6 +78,11 @@
video-editing-light = true;
video-editing-heavy = true;
};
hive.programs.spotify-shortcuts = {
enable = true;
clientIdSopsKey = config.sops.secrets.spotifyShortcutsClientId.name;
clientSecretSopsKey = config.sops.secrets.spotifyShortcutsClientSecret.name;
};
# system packages
environment.systemPackages = with pkgs; [
@ -85,6 +98,7 @@
feh
firefox
git
gramps
insomnia
libreoffice
mosquitto
@ -114,6 +128,9 @@
services.udev.packages = [pkgs.openhantek6022];
virtualisation.docker.enable = true;
# Corsair drivers
hardware.ckb-next.enable = true;
# dpi correction
services.xserver.dpi = 91;
environment.variables = {

View File

@ -18,12 +18,14 @@
# pure system modules
./desktop/de
./desktop/dm
./desktop/themes
./hardware/bluetooth.nix
./hardware/sound.nix
./hardware/yubikey.nix
./networking/wireguard
./programs/creative.nix
./programs/games.nix
./programs/spotify-shortcuts.nix
./services/borg-server.nix
./services/kdeconnect.nix
./services/nextcloud-instance.nix

View File

@ -0,0 +1,5 @@
{...}: {
imports = [
./layan.nix
];
}

View File

@ -0,0 +1,21 @@
{
lib,
config,
pkgs,
...
}: let
cfg = config.hive.themes.layan;
in {
options.hive.themes.layan = {
enable = lib.mkEnableOption "Layan theme configuration";
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [
pkgs.hive.layan-qt6
pkgs.kdePackages.qtstyleplugin-kvantum
pkgs.unstable.layan-cursors
pkgs.layan-gtk-theme
pkgs.tela-circle-icon-theme
];
};
}

View File

@ -140,6 +140,9 @@
(setq ccls-initialization-options '(:index (:comments 2) :completion (:detailedLabel t) :compilationDatabaseDirectory "build" :cache (:directory ".cache/ccls" :format "binary")))
(set-lsp-priority! 'ccls 1))
(setq apheleia-formatters-respect-indent-level nil)
(defun my-ui-doc-glance-then-focus ()
"Glances UI-Doc if not present, otherwise focus"
(interactive)
@ -244,6 +247,8 @@
(bash-ts-mode . lsp-deferred)
(json-ts-mode . lsp-deferred)))
(setq auto-mode-alist (cons '("\\.smt$" . smtlib-mode) auto-mode-alist))
(autoload 'smtlib-mode "smtlib" "Major mode for SMTLIB" t)
(let ((config-dir (expand-file-name "config.d" doom-user-dir)))
(when (file-directory-p config-dir)

View File

@ -62,6 +62,13 @@
:local-repo "ws-butler")
:pin "9ee5a7657a22e836618813c2e2b64a548d27d2ff")
(package! smtlib-mode
:recipe (:host github
:repo "chsticksel/smtlib-mode"
:branch "master"
:local-repo "smtlib-mode")
:pin "ed387e63b64091228e6a8a429b02b8fba165f5b5")
(package! direnv)
(package! pdf-tools)
(package! eww)

View File

@ -5,21 +5,12 @@
...
}: let
cfg = config.hive.themes.layan;
layan-qt6 = pkgs.kdePackages.callPackage ./layan-qt6.nix {};
in {
options.hive.themes.layan = {
enable = lib.mkEnableOption "Layan theme configuration";
};
config = lib.mkIf cfg.enable {
home.packages = [
layan-qt6
pkgs.kdePackages.qtstyleplugin-kvantum
pkgs.unstable.layan-cursors
pkgs.layan-gtk-theme
pkgs.tela-circle-icon-theme
];
qt.enable = false;
qt.style.name = "kvantum";
qt.style.package = pkgs.kdePackages.qtstyleplugin-kvantum;

View File

@ -49,7 +49,13 @@ in {
environment.systemPackages = with pkgs;
lib.optionals cfg.image-editing [gimp krita drawio]
++ lib.optional cfg.image-management digikam
++ lib.optionals cfg.image-raw-processing [darktable hdrmerge]
++ lib.optionals cfg.image-raw-processing [
darktable
rawtherapee
enblend-enfuse
hdrmerge
hugin
]
++ lib.optionals cfg.video-editing-light [ffmpeg losslesscut-bin]
++ lib.optionals cfg.video-editing-heavy [
davinci-resolve

View File

@ -0,0 +1,36 @@
{
config,
lib,
pkgs,
...
}: let
cfg = config.hive.programs.spotify-shortcuts;
in {
options.hive.programs.spotify-shortcuts = {
enable = lib.mkEnableOption "Enable Spotify Shortcuts";
clientIdSopsKey = lib.mkOption {
type = lib.types.singleLineStr;
description = "Spotify API Client ID sops secret name";
};
clientSecretSopsKey = lib.mkOption {
type = lib.types.singleLineStr;
description = "Spotify API Client Secret Path sops secret name";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [pkgs.hive.spotify-shortcuts];
environment.variables = {
SPOTIFY_SHORTCUTS_CONFIG = config.sops.templates."spotify-shortcuts-client.json".path;
};
sops.templates."spotify-shortcuts-client.json" = {
mode = "444";
content = ''
{
"clientId": "${config.sops.placeholder.${cfg.clientIdSopsKey}}",
"clientSecret": "${config.sops.placeholder.${cfg.clientSecretSopsKey}}"
}
'';
};
};
}

View File

@ -2,5 +2,7 @@ final: _: {
hive = {
crossover = final.callPackage ./crossover.nix {};
transcode-davinci-resolve = final.callPackage ./transcode-davinci-resolve {};
spotify-shortcuts = final.callPackage ./spotify-shortcuts {};
layan-qt6 = final.kdePackages.callPackage ./layan-qt6.nix {};
};
}

View File

@ -8,7 +8,7 @@
plasma-workspace,
}:
stdenv.mkDerivation rec {
pname = "layan-kde";
pname = "layan-kde-qt6";
version = "2025-02-13";
src = fetchFromGitHub {
owner = "vinceliuice";

View File

@ -0,0 +1 @@
use flake ../../#spotify-shortcuts --show-trace

View File

@ -0,0 +1,2 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.callPackage ./derivation.nix {}

View File

@ -0,0 +1,7 @@
{python3Packages}:
with python3Packages;
buildPythonApplication {
name = "spotify-shortcuts";
propagatedBuildInputs = [spotipy pyxdg desktop-notifier];
src = ./.;
}

View File

@ -0,0 +1,16 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
setup(
name="spotify_shortcuts",
version="1.0",
packages=find_packages(),
entry_points={
"console_scripts": [
"spotisc=spotify_shortcuts.run:main",
"spotify-like=spotify_shortcuts.spotify_like:main",
"spotify-pl-add=spotify_shortcuts.spotify_pl_add:main",
],
},
)

View File

@ -0,0 +1,15 @@
{pkgs ? import <nixpkgs> {}}: let
drv = pkgs.callPackage ./derivation.nix {};
in
pkgs.mkShell {
packages = [
pkgs.pyright
pkgs.black
];
inputsFrom = [drv];
shellHook = ''
export PYTHONPATH="$PYTHONPATH:$(pwd)"
'';
}

View File

@ -0,0 +1,4 @@
from spotify_shortcuts.run import main
if __name__ == "__main__":
main()

View File

@ -0,0 +1,74 @@
from pathlib import Path
from dataclasses import dataclass
from argparse import ArgumentParser
from typing import List
from os import getenv
from sys import argv
from xdg.BaseDirectory import xdg_cache_home
from json import loads
@dataclass
class Config:
cache_file: Path = Path(xdg_cache_home) / Path("spotify-shortcuts.json")
client_id: str | None = None
client_secret: str | None = None
notifications: bool = True
@staticmethod
def from_file(path: Path):
if not path.exists():
raise FileNotFoundError(f"Configuration file {path} does not exist.")
with open(path, "r") as f:
data = loads(f.read())
return Config(
cache_file=Path(data.get("cacheFile", Config.cache_file)),
client_id=data.get("clientId", Config.client_id),
client_secret=data.get("clientSecret", Config.client_secret),
)
def load_config(args: List[str] = argv[1:]) -> Config:
parser = ArgumentParser(description="Spotify CLI Tool")
parser.add_argument(
"--cache-file",
type=Path,
default=Config.cache_file,
help="Path to the cache file for Spotify authentication",
)
parser.add_argument(
"--client-id",
type=str,
default=Config.client_id,
help="Spotify API Client ID",
)
parser.add_argument(
"--client-secret",
type=str,
default=Config.client_secret,
help="Spotify API Client Secret",
)
parser.add_argument(
"--config-file",
type=str,
help="Path to a json configuration file with keys clientId and clientSecret",
)
parser.add_argument(
"--no-notifications",
action="store_true",
help="Disable desktop notifications",
)
ns = parser.parse_args(args)
cfg = Config()
if (cfg_file := ns.config_file or getenv("SPOTIFY_SHORTCUTS_CONFIG", None)) != None:
cfg = Config.from_file(Path(cfg_file))
return Config(
cache_file=ns.cache_file or cfg.cache_file,
client_id=ns.client_id or cfg.client_id,
client_secret=ns.client_secret or cfg.client_secret,
notifications=not ns.no_notifications or cfg.notifications,
)

View File

@ -0,0 +1,7 @@
from spotify_shortcuts.spotify_like import SpotifyLike
from spotify_shortcuts.spotify_pl_add import SpotifyPlAdd
SHORTCUT_REGISTRY = {
"like": SpotifyLike(),
"pl_add": SpotifyPlAdd(),
}

View File

@ -0,0 +1,47 @@
from spotify_shortcuts.config import Config, load_config
from spotify_shortcuts.shortcut import Shortcut
from spotify_shortcuts.spotify_auth import authenticated_session
from itertools import chain
def all_scopes() -> list[str]:
from spotify_shortcuts.registry import SHORTCUT_REGISTRY
return list(
set(
chain.from_iterable(
shortcut.get_scopes() for shortcut in SHORTCUT_REGISTRY.values()
)
)
)
def run_shortcut(shortcut: Shortcut, config: Config):
client = authenticated_session(
config, scopes=all_scopes() # use all scopes to avoid re-authentication
)
shortcut.execute(client, config)
def main():
from spotify_shortcuts.registry import SHORTCUT_REGISTRY
import sys
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <shortcut_name>")
sys.exit(1)
shortcut_name = sys.argv[1]
if shortcut_name not in SHORTCUT_REGISTRY:
print(f"Shortcut '{shortcut_name}' not found.")
sys.exit(1)
shortcut = SHORTCUT_REGISTRY[shortcut_name]
config = load_config(args=sys.argv[2:])
run_shortcut(shortcut, config)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,21 @@
from abc import ABC, abstractmethod
from spotify_shortcuts.config import Config
from spotipy import Spotify
class Shortcut(ABC):
@abstractmethod
def execute(self, client: Spotify, config: Config):
"""Execute the shortcut action."""
pass
@abstractmethod
def get_help(self) -> str:
"""Return a description of the shortcut."""
pass
@abstractmethod
def get_scopes(self) -> list[str]:
"""Return the spotify API scopes required for the shortcut."""
pass

View File

@ -0,0 +1,20 @@
from spotipy.oauth2 import SpotifyOAuth
from spotipy import Spotify
from spotify_shortcuts.config import Config
CALLBACK_URI = "http://127.0.0.1:45632/callback"
def authenticated_session(cfg: Config, scopes: list[str]) -> Spotify:
assert cfg.client_id, "Spotify client ID is required"
assert cfg.client_secret, "Spotify client secret is required"
return Spotify(
auth_manager=SpotifyOAuth(
client_id=cfg.client_id,
client_secret=cfg.client_secret,
redirect_uri=CALLBACK_URI,
scope=" ".join(scopes),
cache_path=cfg.cache_file,
)
)

View File

@ -0,0 +1,46 @@
from spotify_shortcuts.run import run_shortcut
from spotify_shortcuts.shortcut import Shortcut
from spotify_shortcuts.config import load_config
from desktop_notifier import DesktopNotifierSync
SCOPES = [
"user-library-read",
"user-library-modify",
"user-read-playback-state",
]
class SpotifyLike(Shortcut):
def execute(self, client, config):
if (playback := client.current_playback()) is None:
print("No current playback found.")
return
if (uri := playback.get("item", {}).get("uri", None)) is None:
print("No track URI found in current playback.")
return
client.current_user_saved_tracks_add(tracks=[uri])
if config.notifications:
dn = DesktopNotifierSync()
dn.send(
title="Track Liked",
message=f"Track \"{playback.get('item', {}).get('name', '<no-name>')}\" by \"{
", ".join(a.get('name', '<no-name>') for a in playback.get('item', {}).get('artists', []))
}\" has been liked.",
)
def get_help(self) -> str:
return "Like the currently playing track."
def get_scopes(self) -> list[str]:
return SCOPES
def main():
run_shortcut(SpotifyLike(), load_config())
if __name__ == "__main__":
main()

View File

@ -0,0 +1,57 @@
from spotify_shortcuts.shortcut import Shortcut
from spotify_shortcuts.config import load_config
from desktop_notifier import DesktopNotifierSync
from spotify_shortcuts.run import run_shortcut
SCOPES = [
"playlist-modify-public",
"playlist-modify-private",
"user-read-playback-state",
]
class SpotifyPlAdd(Shortcut):
def execute(self, client, config):
if (playback := client.current_playback()) is None:
print("No current playback found.")
return
if (track_uri := playback.get("item", {}).get("uri", None)) is None:
print("No track URI found in current playback.")
return
if (context_uri := playback.get("context", {}).get("uri", None)) is None:
print("No context URI found in current playback.")
return
client.playlist_add_items(context_uri, items=[track_uri])
if config.notifications:
track_name = playback.get("item", {}).get("name", "<no-name>")
artists = ", ".join(
a.get("name", "<no-name>")
for a in playback.get("item", {}).get("artists", [])
)
playlist_name = (client.playlist(context_uri) or {}).get(
"name", "<no-name>"
)
dn = DesktopNotifierSync()
dn.send(
title="Track Added to Playlist",
message=f'Track "{track_name}" by "{artists}" has been added to {playlist_name}.',
)
def get_help(self) -> str:
return "Add the currently playing track to the current playlist."
def get_scopes(self) -> list[str]:
return SCOPES
def main():
run_shortcut(SpotifyPlAdd(), load_config())
if __name__ == "__main__":
main()

View File

@ -0,0 +1,44 @@
clientId: ENC[AES256_GCM,data:YJYM62aqGafgqsS2rFQZlS8REuR21biiGtQUaUG18Qg=,iv:TIL+rlKbBVo2/JlaGRDqLFwQ+ETx3jkqSqmGu6kyvJc=,tag:0J29BXv0kppcmqm5xbAvew==,type:str]
clientSecret: ENC[AES256_GCM,data:3pw6QRC+cjZlQ3iGnzLVjQh3sHPld6UC2jC8yq+J+34=,iv:9xctJRA4RFGy7x18Y2aPVuSC1lZ0Rko0R8Hr/gE2YIw=,tag:nB4VENRf0YeZCpMM5QFA9Q==,type:str]
sops:
age:
- recipient: age1expg8vyduf290pz7l4f3mjzvk9f0azfdn48pyjzs3m6p7v4qjq0qwtn36z
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBodVlLVW5PODFLcUh2anlU
Yys2czJSVzc3cHdpL3RZU01HcXhEUkNRcVV3CjcvNTROVStid2x0TUt6eEpQanJD
NkNSTlVkLzBqMUZQSUdHQyt4VUZZQW8KLS0tIEZiTm9HNUdtelNrVTUwSmFnb0p5
K2NsMnZ3WDJIWWpmYml5cjNWZWFPWEUKfP9HxpJ35R/SiI5lzdRiVPIJUnhdbPJl
OtEUHJOrAYnKgX0uOo1argaQxN/8V0Py5njSdiiLd+mw1OCY4xef8w==
-----END AGE ENCRYPTED FILE-----
- recipient: age1wf0rq27v0n27zfy0es8ns3n25e2fdt063dgn68tt3f89rgrtu9csq4yhsp
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLY1N5aXZzRzVaU2QzcmxS
cjBTSlA1dlNMRGhhRDUrQWphR2ZJcHhTZjBBCmxjR3g0NVc4UUxBV25rNFVCSzhM
Tk5xeHZtZGkxRlpwRElZa0c4eUFKcXcKLS0tIHlCSXBKQ3BSSWNEeklBdmw1VHBI
RHVwTmd2dDlWSkZia2t6cE01bGxKUUkKQ9QuzfhuoGd7x42p5bP2E8Tatw1Ihqu2
XFem9XLMIX1gNqRJkL7DmVybI/hE34pcAhALUKEG/K1vr51I/nKdGA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1xkmnvzus6fhundn4c0f6hyuwrj0f0m7x3hxtuhnez6cecr6m032qalw308
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMVWFwcE5GcHVnQzlCa2hz
UXlsMEFBRURlUWdRTXJUcktFRytzV0VWK3kwCnFDc0U3MzV1Mnhvek1DWmtvVXdW
bEJVM3ZpSm9IR2F2d0FtZHptOHlwWVEKLS0tIEJrL2YycVFGbGNuQ0JQZy80aFJa
YnNjSi9lSm1VWDlRRnJwb2U4cUhPWHcKXUB9ExCEgw29CvbT/OHZJa9F+DChep3r
twET8VbrV19O5zscu0OJ5s7hrobm+eWzPkoYlBXPC3bfwgeRRbsIvA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1clh2c489j7mx94aqr44u6k2cx5axqme9rlshqu9l2mcynluwhq6qwn0sv0
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqWENabXAwRlF0QmlRMGlS
SGx2UEFYcXJ6aWE0TDlrQ3FQNXFucTlVQXlBCmZSRERNMlFacUNDcHRtdVd1aFlh
WElZenhMT3JBMUhFK21MOEJhT083TVEKLS0tIEFHNzZZUzJtTkE3TEZNMlJreG1l
cHQxWXFCWlkvUFRvWUZycTdWMWRVMUEKN0jKYMQ9ARfnUUXprSYWdAAbVdOng8Yn
nV9sKYlTQ4fFBZ7w8hIBoJP3pWdIuZfEwKgA/7OyE02dts5wxIAuQw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-07-23T23:55:41Z"
mac: ENC[AES256_GCM,data:dS+rBxe/+Qce4rqQB8dhtuMmAnmUzvXOXPMTWH2tglxxdYcJrQgatGC+0qNdJIa4vEi1cb9IcHiQE4fv615OSQuc4uDIPzLyLK7eDFz9DkK/eXehK6V7t1ZUPYKiSswZYLBcApJTQFYzaG5OqcFm7rcFIz4toXo4TNM94s0dRH8=,iv:CWv9zYvMOn3qvkHnpT+Bk67VJbQs5daMxjpjYm6dr8I=,tag:+0ENRlYqliohdf7ytH7IcA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.10.2