-
-
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 …
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:For Python 3 requirements, run:
Steps to Update the systemd file:
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.
Reload systemd
After making changes to the service file, reload systemd to apply the changes:
Restart or Stop the Service
Now you can stop the service gracefully by calling:
This will run the script with the
-k
argument, terminating the daemon process.Start the Service Again
If you want to restart the service: