Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
molneya committed Sep 25, 2024
0 parents commit f87be9f
Show file tree
Hide file tree
Showing 14 changed files with 675 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

.pyinstaller/
.venv/
__pycache__/
build/
data/
dist/
icons/
lists/

player.db
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@

# unnamed anime song player

A simple player to play anime songs.

## Setup

This guide assumes you're using Windows. If you use something else, good luck.

### Player

By default, this music player uses [mpv](https://mpv.io/) to play music. To install (including to PATH), I'd recommend first installing [Chocolatey](https://chocolatey.org/install#individual), then installing [mpv](https://community.chocolatey.org/packages/mpv) using Chocolatey.

For a list of mpv shortcuts to use during song playback, check the mpv [manual](https://mpv.io/manual/stable/#interactive-control). TL;DR use `0` or `9` to adjust volume, `Space` to pause and `q` to end playback of the current song.

### Rainmeter

To help with anime song memorisation, this music player can output the current song to a file, where it can be read by [Rainmeter](https://www.rainmeter.net/) to display it on the screen.

To use the provided overlay of the current playing song using Rainmeter, you can make a symlink using the command below to add the skin to Rainmeter (requires admin on Windows 10).
```
cd path/to/this/music/player/folder
mklink /d "%USERPROFILE%\Documents\Rainmeter\Skins\CurrentlyPlaying" "%CD%\skins\CurrentlyPlaying"
```

You may also need to refresh the skin to make it show up properly because Rainmeter can be funny sometimes.

### Lists

Before playing songs, you need to download a list. Use [AnisongDB](https://anisongdb.com/) to search and download `.json` files. You can place these files into a folder (let's call it `lists`), or into subdirectories for better organisation.

## Usage

### Starting the player

#### Lists

To play lists (aka those `.json` files) inside the `lists` directory, use the following command:
```player --list lists```

Note the above command does not search folders recursively. If you've put files into subdirectories, you need to specify them:
```player --list lists\firstlist lists\someotherlist```

The player also understands individual files:
```player --list lists\overlord_ii.json lists\overlord_iv.json```

#### Options

There are many options to adjust how you want the player to behave. You can either make changes to the `options.conf` file, or specify command line arguments. View those command line arguments by using
```player --help```

The above lists example can instead be done through the `options.conf` file, for example.

### Playback

To go back a song, close the current player (`q` by default) and quickly press `q`. Simply, just double tap the `q` button.

To end the playlist, close the current player and quickly press `Escape`.
6 changes: 6 additions & 0 deletions build.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
rmdir /s /q dist
:: pyinstaller player.py --onefile --icon icons\icon.ico
pyinstaller player.py --onefile
copy README.md dist\README.md
copy options.conf dist\options.conf
xcopy /s /e /h /I /q skins dist\skins
69 changes: 69 additions & 0 deletions database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@

import logging, sqlite3
from datetime import datetime

class Connection:
def __init__(self, path):
self.connect = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES)

def __enter__(self):
return self.connect.cursor()

def __exit__(self, exc_type, exc_val, exc_tb):
self.connect.commit()
self.connect.close()

class Database:
def __init__(self, path):
self.path = path

def initalise(self):
with Connection(self.path) as con:
con.execute(
"""
CREATE TABLE IF NOT EXISTS songs (
hash int NOT NULL PRIMARY KEY,
play_count INTEGER NOT NULL,
last_played TIMESTAMP NOT NULL
)
"""
)

logging.debug("Initialised database")

def update(self, song):
with Connection(self.path) as cursor:
now = datetime.now()
cursor.execute(
"""
INSERT OR REPLACE INTO songs
VALUES (?, 1, ?)
ON CONFLICT (hash) DO
UPDATE SET
play_count = play_count + 1,
last_played = ?
""",
(hash(song), now, now)
)

logging.debug(f"Updated database: {hash(song)}")

def select(self, song):
with Connection(self.path) as cursor:
result = cursor.execute(
"""
SELECT
play_count,
last_played
FROM songs
WHERE hash = ?
""",
(hash(song),)
).fetchone()

logging.debug(f"Retrieved from database: {hash(song)}: {result}")

if result:
return result

return 0, None
13 changes: 13 additions & 0 deletions getch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

import msvcrt, time

def getch_or_timeout(timeout):
'''
Waits for `timeout` seconds to check for a user input
'''
time.sleep(timeout)

if msvcrt.kbhit():
return msvcrt.getch()

return
32 changes: 32 additions & 0 deletions options.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

### Values shown below are all defaults.
### If you want to change the option, uncomment the line and change the value after the equals sign.

# lists to play, can be either directories or files
#lists=

# the audio player to use
#player=mpv --no-video

# output file of the currently playing song
#output=skins\CurrentlyPlaying\CurrentlyPlaying.txt

# show titles in english
#prefer_english=0

# use the program without features requiring an internet connection
#offline_mode=0

# level of logs to show. Accepted values: DEBUG, INFO, WARNING, ERROR, CRITICAL
#log_level=WARNING

# difficulty of songs to play
#min_difficulty=0
#max_difficulty=100

# search for things to play
#search_artists=
#search_anime=

# show results for exact searches only
#exact_search=0
86 changes: 86 additions & 0 deletions options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@

import argparse, pathlib, logging, shlex

class Options:
def __init__(self):
self.lists = []
self.player = "mpv --no-video"
self.output = r"skins\CurrentlyPlaying\CurrentlyPlaying.txt"
self.prefer_english = False
self.offline_mode = False
self.log_level = "WARNING"
self.min_difficulty = None
self.max_difficulty = None
self.search_artists = []
self.search_anime = []
self.exact_search = False

def from_file(self, file_path):
'''
Sets options from file. This will overwrite the default options.
'''
with open(file_path, 'r') as f:
for line in f.readlines():
line = line.strip()

# Skip lines without content or comments
if not line or line.startswith('#'):
continue

# Unpack key/value pairs, skipping if we get something invalid
try:
key, value = line.split('=', 1)
except ValueError:
continue

try:
match key.lower():
# String values
case 'player' | 'output' | 'log_level':
setattr(self, key, value)
# Switches
case 'prefer_english' | 'offline_mode' | 'exact_search':
setattr(self, key, bool(int(value)))
# Floats
case 'min_difficulty' | 'max_difficulty':
setattr(self, key, float(value))
# Lists
case 'lists' | 'search_artists' | 'search_anime':
setattr(self, key, shlex.split(value))
# Ignore anything else
case _:
print(f"Ignoring unknown option: '{key}'")

except (ValueError, AttributeError):
print(f"Ignoring invalid value for option {key}: '{value}'")

def from_options(self):
'''
Sets options from command line arguments. This will overwrite options loaded from file.
'''
parser = argparse.ArgumentParser()
parser.add_argument("-l", "--list", default=self.lists, type=pathlib.Path, nargs='+', help="lists to play, can be either directories or files")
parser.add_argument("-p", "--player", default=self.player, type=str, help="the audio player to use")
parser.add_argument("-o", "--output", default=self.output, type=pathlib.Path, help="output file of the currently playing song")
parser.add_argument("--prefer-english", default=self.prefer_english, action="store_true", help="show titles in english")
parser.add_argument("--offline-mode", default=self.offline_mode, action="store_true", help="use the program without features requiring an internet connection")
parser.add_argument("--log-level", default=self.log_level, type=str, help="level of logs to show")
parser.add_argument("--min-difficulty", default=self.min_difficulty, type=float, metavar="MIN", help="minimum song difficulty to play")
parser.add_argument("--max-difficulty", default=self.max_difficulty, type=float, metavar="MAX", help="maximum song difficulty to play")
parser.add_argument("--search-artists", default=self.search_artists, type=str, nargs='*', metavar="ARTIST", help="search for artists to play from")
parser.add_argument("--search-anime", default=self.search_anime, type=str, nargs='*', metavar="ANIME", help="search for anime to play from")
parser.add_argument("--exact-search", default=self.exact_search, action="store_true", help="show results for exact searches only")
args = parser.parse_args()

# Set values
self.lists = args.list
self.player = args.player
self.output = args.output
self.prefer_english = args.prefer_english
self.offline_mode = args.offline_mode
self.log_level = args.log_level
self.min_difficulty = args.min_difficulty
self.max_difficulty = args.max_difficulty
self.search_artists = args.search_artists
self.search_anime = args.search_anime
self.exact_search = args.exact_search
31 changes: 31 additions & 0 deletions player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

import argparse, logging, os, pathlib
from database import Database
from options import Options
from playlist import Playlist

def main():
# Parse player options from file and command line
options = Options()
options.from_file("options.conf")
options.from_options()

# Create logger
logger = logging.getLogger(__name__)
log_level = getattr(logging, options.log_level, 30)
logging.basicConfig(level=log_level, format="[%(levelname)s] %(message)s")

# Ensure we have correct directories
os.makedirs("data", exist_ok=True)

# Set up database
db = Database("player.db")
db.initalise()

# Start playlist
playlist = Playlist(options, db)
playlist.create()
playlist.play()

if __name__ == "__main__":
main()
38 changes: 38 additions & 0 deletions player.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
['player.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='player',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
Loading

0 comments on commit f87be9f

Please sign in to comment.