Skip to content

Instantly share code, notes, and snippets.

@bonelifer
Forked from Pigpog/skipdetect.sh
Last active January 26, 2025 02:59
Show Gist options
  • Save bonelifer/92760f8a13ba7e88e559548201e305d0 to your computer and use it in GitHub Desktop.
Save bonelifer/92760f8a13ba7e88e559548201e305d0 to your computer and use it in GitHub Desktop.
FOR MPD - Skips songs you don't like. Uses a per-song score, calculated based on whether you skip the song or let it play. The only dependencies are `mpc` and `argparse(python module)` , as this script is implemented entirely in Python 3. When the next song's score is negative, the script "rolls the dice," and if the score is less than or equal …
[Unit]
Description=MPD Song Skipper
After=mpd.service
Wants=mpd.service
[Service]
ExecStart=/usr/bin/python3 /path/to/your/script/skipdetect.py -d
ExecStop=/usr/bin/python3 /path/to/your/script/skipdetect.py -k
Restart=on-failure
RestartSec=5
StandardOutput=null
StandardError=journal
Environment="DBFILE=/path/to/your/scripts-database/database"
[Install]
WantedBy=default.target
#!/usr/bin/python3
"""
MPD Song Skipper Script
This script automatically skips or updates the scores of songs played using MPD (Music Player Daemon).
Features include:
- Skipping songs based on their score.
- Updating the song score depending on playback time:
* Skipping a song decreases its score.
* Playing a song for more than 15 seconds increases its score.
- Daemon mode to run the script in the background.
- Kill mode to terminate the script when running in daemon mode.
- Verbose mode for detailed logging.
Dependencies:
- `mpc` command-line utility for interacting with MPD.
- A database file (`DBFILE`) to store song scores.
Arguments:
- `-v, --verbose`: Enable verbose logging.
- `-d, --daemon`: Run the script as a background daemon process.
- `-k, --kill`: Terminate the running daemon process.
PID File:
- The script uses `/tmp/mpd_skipper.pid` to track the daemon process.
Database Format:
- Tab-separated values with each line as "<score>\t<title>".
"""
import os
import random
import subprocess
import time
import argparse
import sys
import signal
# Path to the database file
DBFILE = "/home/william/CODE/mpd/othr/database"
# Path to the PID file
PIDFILE = "/tmp/mpd_skipper.pid"
def sanitize(text):
"""Remove square brackets from the input string."""
return text.replace("[", "").replace("]", "")
def search(database, pattern):
"""
Search for a pattern in the database file.
Returns the matching line or None if not found.
Args:
database (str): Path to the database file.
pattern (str): The song title to search for.
Returns:
str or None: The matching database line or None.
"""
with open(database, "r") as db:
for line in db:
score, title = line.strip().split("\t", 1)
if title == pattern:
return line.strip()
return None
def update_database(database, old_entry, new_entry):
"""
Update the database, replacing an old entry with a new one.
If the old entry doesn't exist, add the new entry.
Args:
database (str): Path to the database file.
old_entry (str): The existing database line to replace.
new_entry (str): The new database line to add.
"""
if old_entry:
# Replace the old entry with the new one
with open(database, "r") as db:
lines = db.readlines()
with open(database, "w") as db:
for line in lines:
if line.strip() == old_entry:
db.write(new_entry + "\n")
else:
db.write(line)
else:
# Add the new entry if it doesn't exist
with open(database, "a") as db:
db.write(new_entry + "\n")
def daemonize():
"""
Daemonize the process using the double-fork method.
Creates a PID file to track the process.
"""
try:
# First fork: Detach from the parent process
pid = os.fork()
if pid > 0:
sys.exit(0) # Exit parent process
except OSError as e:
print(f"Fork #1 failed: {e}", file=sys.stderr)
sys.exit(1)
os.setsid() # Create a new session
try:
# Second fork: Prevent reacquisition of a controlling terminal
pid = os.fork()
if pid > 0:
sys.exit(0) # Exit first child process
except OSError as e:
print(f"Fork #2 failed: {e}", file=sys.stderr)
sys.exit(1)
# Redirect standard file descriptors to `/dev/null`
sys.stdout.flush()
sys.stderr.flush()
with open("/dev/null", "r") as fnull:
os.dup2(fnull.fileno(), sys.stdin.fileno())
with open("/dev/null", "a") as fnull:
os.dup2(fnull.fileno(), sys.stdout.fileno())
os.dup2(fnull.fileno(), sys.stderr.fileno())
# Write the process ID to the PID file
with open(PIDFILE, "w") as pidfile:
pidfile.write(str(os.getpid()) + "\n")
def kill_daemon():
"""
Terminate the running daemon process using the PID file.
"""
if os.path.exists(PIDFILE):
with open(PIDFILE, "r") as pidfile:
pid = int(pidfile.read().strip())
try:
os.kill(pid, signal.SIGTERM) # Send termination signal
print(f"Daemon process {pid} terminated.")
except ProcessLookupError:
print(f"No process with PID {pid} found.")
except PermissionError:
print(f"Permission denied to kill process {pid}.")
os.remove(PIDFILE) # Remove PID file
else:
print("No PID file found. Is the daemon running?")
def main(verbose):
"""
Main loop for managing song playback and score updates.
Args:
verbose (bool): Enable verbose logging.
"""
while True:
skip_next = False
# Get currently playing song
now_playing = sanitize(subprocess.getoutput("mpc current -f '%artist% * %title% * %album%'"))
if not now_playing:
now_playing = "!!!!!!!!!!!!!!!!" # Placeholder for no song playing
# Retrieve the score from the database
db_line = search(DBFILE, now_playing)
score = int(db_line.split("\t")[0]) if db_line else 0
# Verbose logging
if verbose:
print(f"Song: {now_playing}")
print(f"Score: {score}")
# Check queued song and decide whether to skip
queued = sanitize(subprocess.getoutput("mpc queued"))
queued_line = search(DBFILE, queued)
if queued_line and int(queued_line.split("\t")[0]) <= -random.randint(0, 5):
skip_next = True
if verbose:
print(f"Skipping next: {queued}")
# Wait for the song to change and calculate playtime
start_time = time.time()
subprocess.run("mpc current --wait", shell=True, stdout=subprocess.DEVNULL)
time_played = time.time() - start_time
# Handle skipping the queued song
if skip_next and sanitize(subprocess.getoutput("mpc current -f '%artist% * %title% * %album%'")) == queued:
subprocess.run("mpc next -q", shell=True)
continue
# Update score based on playtime
if time_played < 15:
if verbose:
print(f"Played {now_playing} for less than 15 seconds")
new_score = score - 1
else:
if verbose:
print(f"Played {now_playing} for more than 15 seconds")
new_score = score + 1
# Update or add the entry in the database
new_entry = f"{new_score}\t{now_playing}"
update_database(DBFILE, db_line, new_entry)
if verbose:
if not db_line:
print("Not found in database; adding")
else:
print("Updating existing database entry")
if __name__ == "__main__":
# Argument parser configuration
parser = argparse.ArgumentParser(description="Automatically skip songs based on their score.")
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging")
parser.add_argument("-d", "--daemon", action="store_true", help="Run the script as a daemon (background process)")
parser.add_argument("-k", "--kill", action="store_true", help="Kill the running daemon process")
args = parser.parse_args()
# Handle daemon termination
if args.kill:
kill_daemon()
sys.exit(0)
# Start in daemon mode if specified
if args.daemon:
daemonize()
# Run the main function
main(args.verbose)
@bonelifer
Copy link
Author

bonelifer commented Jan 26, 2025

FOR MPD - Skips songs you don't like. Uses a per-song score, calculated based on whether you skip the song or let it play. The only dependency is mpc, as this script is implemented entirely in Python 3. When the next song's score is negative, the script "rolls the dice," and if the score is less than or equal to the dice roll, it will skip the song when it comes on.

The script supports running in verbose mode for detailed logs, background daemon mode for continuous operation, and a kill mode to terminate the daemon when necessary.

Requirements

Dependency Installation

Ensure mpc is installed for the script to interact with MPD. Install it using your package manager:

sudo apt install mpc

For Python 3 requirements, run:

pip install argparse

Steps to Update the systemd file:

  1. Update the Service File
    Edit the systemd service file as follows:

    nano ~/.config/systemd/user/mpd_skipper.service

    Edit ExecStart, ExecStop, DBFILE(database path) to point to your files.

  2. Reload systemd
    After making changes to the service file, reload systemd to apply the changes:

    systemctl --user daemon-reload
  3. Restart or Stop the Service
    Now you can stop the service gracefully by calling:

    systemctl --user stop mpd_skipper.service

    This will run the script with the -k argument, terminating the daemon process.

  4. Start the Service Again
    If you want to restart the service:

    systemctl --user restart mpd_skipper.service

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment