├── scrobbler ├── __init__.py ├── info.py └── scrobbler.py ├── .gitignore ├── MANIFEST.in ├── .github └── FUNDING.yml ├── CHANGES.txt ├── setup.py ├── README.md └── LICENSE /scrobbler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include CHANGES.txt 3 | 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: hauzer 4 | liberapay: hauzer 5 | issuehunt: hauzer 6 | -------------------------------------------------------------------------------- /scrobbler/info.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # 4 | # A command-line Last.fm scrobbler and a now-playing status updater. 5 | # Copyright (C) 2013 Никола "hauzer" Вукосављевић 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | 21 | 22 | NAME = "scrobbler" 23 | VERSION = "1.1.4" 24 | 25 | AUTHOR_FIRST_NAME = "Никола" 26 | AUTHOR_LAST_NAME = "Вукосављевић" 27 | AUTHOR_NICK = "hauzer" 28 | AUTHOR = "{} \"{}\" {}".format(AUTHOR_FIRST_NAME, AUTHOR_NICK, AUTHOR_LAST_NAME) 29 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v1.1.4, 2017-10-24 2 | - Can now use double dashes (--) in `scrobble` and `now-playing` 3 | commands to separate options and position parameters. 4 | 5 | v1.1.3-1, 2016-10-06 6 | - Fixed some installation metadata handling. 7 | 8 | v1.1.3, 2016-10-06 9 | - add-user, when called without arguments, can be instructed not to 10 | invoke the browser, via -x/--dont-invoke-browser. 11 | - Clarified that Python 3 is required. 12 | - Fixed some short options not working. 13 | 14 | v1.1.2-1, 2014-03-30 15 | - Updated to lfm 1.1.0. 16 | 17 | v1.1.2, 2014-03-30 18 | - Fixed a few significant setup.py issues. 19 | 20 | v1.1.1, 2013-11-19 21 | - --duration can now be in the format of XXhYYmZZs. 22 | - Fixed the how-to. 23 | - Changed the status of the project from Alpha to Beta. 24 | 25 | v1.1.0, 2013-11-08 26 | - Completely revamped the code. The whole experience is now much smoother. 27 | - Using docopt instead of argparse. 28 | 29 | v1.0.3, 2013-07-29 30 | - Fixed a bug. 31 | - Added a user-agent. 32 | 33 | v1.0.2, 2013-07-24 34 | - Cleaned up the output a lot. 35 | 36 | v1.0.1, 2013-07-24 37 | - Fixed a bug where when using the -f option, the time of the scrobble 38 | would be always the current time. 39 | - Made --scrobble a mandatory "option" when using the 'scrobble' command. 40 | 41 | v1.0.0, 2013-07-23 42 | - The initial release. 43 | 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | 4 | # 5 | # A command-line Last.fm scrobbler and a now-playing status updater. 6 | # Copyright (C) 2013 Никола "hauzer" Вукосављевић 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | # 21 | 22 | 23 | from scrobbler import info 24 | from setuptools import setup, find_packages 25 | 26 | 27 | setup(name = "{}h".format(info.NAME), 28 | version = info.VERSION, 29 | packages = find_packages(), 30 | install_requires = ["appdirs", "docopt", "lfmh"], 31 | entry_points = { 32 | "console_scripts": [ 33 | "scrobbler = scrobbler.scrobbler:main" 34 | ], 35 | }, 36 | 37 | author = info.AUTHOR, 38 | author_email = "hauzer.nv@gmail.com", 39 | description = "A command-line Last.fm scrobbler and a now-playing status updater.", 40 | long_description = open("README.rst", "r").read(), 41 | license = "GPLv3", 42 | url = "https://github.com/{}/{}/".format(info.AUTHOR_NICK, info.NAME), 43 | # download_url = "https://bitbucket.org/{}/{}/downloads".format(info.AUTHOR_NICK, info.NAME), 44 | 45 | classifiers = [ 46 | "Development Status :: 4 - Beta", 47 | "Environment :: Console", 48 | "Intended Audience :: End Users/Desktop", 49 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 50 | "Operating System :: OS Independent", 51 | "Programming Language :: Python :: 3", 52 | ], 53 | ) 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > _**ARCHIVAL REASON**_ 3 | > 4 | > I haven't been actively using Last.fm for years now, and I don't expect to come back. 5 | > 6 | > Feel free to fork and continue working on this. If you're serious about it, I'll officially endorse your repo! 7 | 8 | Note: Python 3 is required! 9 | 10 | # Usage 11 | 12 | The program can be invoked with one of the following commands: 13 | 14 | - *add-user* - Add a user to the list of known users. 15 | 16 | > [user] 17 | > A Last.fm username. 18 | > 19 | > [--password, -p] 20 | > The corresponding password. 21 | > 22 | > If a username is provided, you will be prompted for a password. If 23 | > the command is invoked without any arguments, a Last.fm 24 | > authorization web-page will be opened for you to grant access to 25 | > the application. 26 | > 27 | > [--dont-invoke-browser, -x] 28 | > When invoking the command without arguments, always show 29 | > the authentication URL; never try to automatically open 30 | > it. 31 | 32 | - *list-users* - List known users and their corresponding session 33 | keys. 34 | 35 | - *remove-user* - Remove a user from the list of known users. 36 | 37 | > user 38 | > The user to remove. 39 | 40 | - *scrobble* - Scrobble a track. 41 | 42 | > user 43 | > The username to scrobble with. If the user isn\'t known, 44 | > you will be prompted for a password. 45 | > 46 | > artist 47 | > The name of the artist. 48 | > 49 | > track 50 | > The name of the track. 51 | > 52 | > time 53 | > The time of listening. Formatted by \--time-format. It may 54 | > also be *now*, in which case the current time is used. 55 | > 56 | > [--time-format, -tf] 57 | > Specifies the format of *time*, using the syntax of 58 | > [strftime()](http://docs.python.org/dev/library/time.html#time.strftime). 59 | > Defaults to *%Y-%m-%d.%H:%M*. 60 | > 61 | > [--album, -a] 62 | > The name of the album. 63 | > 64 | > [--duration, -d] 65 | > Has the format of XXhYYmZZs. At least one of those has to 66 | > be present, but any number of them can be specified, and 67 | > in any order. 68 | 69 | - *now-playing* - Update the now-playing status. 70 | 71 | > user 72 | > The username to use. If the user isn\'t known, you will be 73 | > prompted for a password. 74 | > 75 | > artist 76 | > The name of the artist. 77 | > 78 | > track 79 | > The name of the track. 80 | > 81 | > [--album, -a] 82 | > The name of the album. 83 | > 84 | > [--duration, -d] 85 | > Has the format of XXhYYmZZs. At least one of those has to 86 | > be present, but any number of them can be specified, and 87 | > in any order. 88 | 89 | # Examples 90 | 91 | Add a user to the list of known users: 92 | 93 | $ scrobbler add-user 94 | The Last.fm authentication page will be opened, or its URL printed here. 95 | Press enter to continue. 96 | Press enter after granting access. 97 | User hauzzer added. 98 | 99 | $ 100 | 101 | and: 102 | 103 | $ scrobbler add-user hauzzer 104 | Password: 105 | User hauzzer added. 106 | 107 | $ 108 | 109 | also: 110 | 111 | $ scrobbler add-user hauzzer --password ****** 112 | User hauzzer added. 113 | 114 | $ 115 | 116 | List all known users: 117 | 118 | $ scrobbler list-users 119 | hauzzer | b431328fc489a4f6e6eeee3e8a0f5537 120 | 121 | $ 122 | 123 | Scrobble a track, \"[Lamplight 124 | Symphony](http://www.last.fm/music/Kansas/_/Lamplight+Symphony)\" by 125 | [Kansas](http://www.last.fm/music/Kansas), which was listened to on 126 | 07/15/2013 at 15:32: 127 | 128 | $ scrobbler scrobble hauzzer Kansas "Lamplight Symphony" 2013-15-07.15:32 -a "Song for America" -d 8m16s 129 | Track scrobbled. 130 | 131 | $ 132 | 133 | Update the now-playing status with \"[Incomudro - Hymn to the 134 | Atman](http://www.last.fm/music/Kansas/_/Incomudro+-+Hymn+to+the+Atman)\" 135 | by [Kansas](http://www.last.fm/music/Kansas).: 136 | 137 | $ scrobbler now-playing hauzzer Kansas "Incomudro - Hymn to the Atman" -a "Song for America" -d 12m17s 138 | Status updated. 139 | 140 | $ 141 | 142 | Remove a user from the list of known users: 143 | 144 | $ scrobbler remove-user hauzzer 145 | User hauzzer removed. 146 | 147 | $ 148 | 149 | # Donations 150 | 151 | If you enjoy my work, please consider a donation. 152 | 153 | > BTC: BC1QF2G847UQTDY6GAG5D64DSCFVEZ0HHY7AC3PNKX 154 | > 155 | > ETH: 0x61a08C3f8dF5A0507923FcA2ec8597e68e51d6A0 156 | > 157 | > XMR: 158 | > 48aLGv9rg2Q1edA36PjKbj34SEAViUSGH47QfGDmWuqEDjUE1fA238BMn6z3R79DfKBTgu6TkT4VL5sMeTG6axMaKXytH6F 159 | -------------------------------------------------------------------------------- /scrobbler/scrobbler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | 4 | # 5 | # A command-line Last.fm scrobbler and a now-playing status updater. 6 | # Copyright (C) 2013 Никола "hauzer" Вукосављевић 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | # 21 | 22 | """ 23 | A command-line Last.fm scrobbler and a now-playing status updater. 24 | 25 | usage: scrobbler [--sessions-file=] [--liblfm-file=] 26 | [--help] [--version] 27 | [...] 28 | 29 | options: 30 | --sessions-file= Specify the path to the database holding known users. 31 | --liblfm-file= Specify the path which the lfm library should use for its file. 32 | 33 | commands: 34 | add-user Add a user to the list of known users. 35 | list-users List all known users. 36 | remove-user Remove a user from the list of known users. 37 | scrobble Scrobble a track. 38 | now-playing Update the now-playing status. 39 | """ 40 | 41 | import sys 42 | if sys.version_info[0] != 3: 43 | print("Python 3 required.") 44 | exit() 45 | 46 | from . import info 47 | 48 | from appdirs import AppDirs 49 | from docopt import docopt 50 | from lastfm import lfm 51 | import lastfm.exceptions 52 | 53 | from getpass import getpass 54 | from datetime import datetime 55 | import time 56 | import os.path 57 | import re 58 | import sqlite3 59 | import webbrowser 60 | 61 | 62 | API_KEY = "b3e7abc138f65a43803f887aeb36b9f6" 63 | SECRET = "d60a1a4d704b71c0e8e5bac98d793969" 64 | 65 | dirs = AppDirs(info.NAME, info.AUTHOR_NICK, info.VERSION) 66 | USERS_DB_FILE = os.path.join(dirs.user_data_dir, "sessions.db") 67 | LIBLFM_FILE = os.path.join(dirs.user_data_dir, "lfm.db") 68 | 69 | 70 | class Error(Exception): 71 | """ 72 | An error intended to be printed for the user. 73 | """ 74 | 75 | def __init__(self, msg): 76 | super().__init__("Error: {}.".format(msg)) 77 | 78 | pass 79 | 80 | 81 | def duration_to_seconds(string): 82 | if string is None: 83 | return None 84 | 85 | durations = re.findall("\d+[hms]", string) 86 | seconds = 0 87 | for duration in durations: 88 | time = int(duration[:-1]) 89 | unit = duration[-1] 90 | 91 | if unit == "m": 92 | time *= 60 93 | elif unit == "h": 94 | time *= 60**2 95 | 96 | seconds += time 97 | 98 | return seconds 99 | 100 | 101 | def db_table_exists_sessions(dbc): 102 | dbc.execute("select exists(select * from sqlite_master " \ 103 | "where type = \"table\" and name = \"sessions\")") 104 | return dbc.fetchone()[0] 105 | 106 | 107 | def db_create_table_sessions(dbc): 108 | dbc.execute("create table sessions (user text primary key, key text)") 109 | 110 | 111 | def user_exists(dbc, user): 112 | dbc.execute("select exists(select * from sessions where user == ?)", (user,)) 113 | return bool(dbc.fetchone()[0]) 114 | 115 | 116 | def exit_if_user_exists(dbc, user): 117 | if user_exists(dbc, user): 118 | raise Error("{} is already registered.".format(user)) 119 | 120 | 121 | def exit_if_user_not_exists(dbc, user): 122 | if not user_exists(dbc, user): 123 | raise Error("{} isn't in the list of known users.".format(user)) 124 | 125 | 126 | def auth(app, dbc, user): 127 | if not user_exists(dbc, user): 128 | pwd = getpass() 129 | session = app.auth.get_mobile_session(user, pwd) 130 | app.session_key = session["key"] 131 | 132 | else: 133 | dbc.execute("select key from sessions where user == ?", (user,)) 134 | app.session_key = dbc.fetchone()[0] 135 | 136 | 137 | def cmd_add_user(app, dbc, args): 138 | """ 139 | Add a user to the list of known users. 140 | 141 | usage: scrobbler add-user [[ [--password=]]|[--dont-invoke-browser]] 142 | 143 | options: 144 | -p , --password= 145 | -x, --dont-invoke-browser When invoking the command without arguments, 146 | always show the authentication URL; never try 147 | to automatically open it. 148 | """ 149 | 150 | if args[""] is None: 151 | token = app.auth.get_token() 152 | 153 | if args["--dont-invoke-browser"]: 154 | print("Last.fm authentication URL: {}".format(token.url)) 155 | else: 156 | input("The Last.fm authentication page will be opened, or its URL printed here.\nPress enter to continue.") 157 | try: 158 | webbrowser.open(token.url) 159 | except webbrowser.Error: 160 | print(token.url) 161 | 162 | input("Press enter after granting access.") 163 | session = app.auth.get_session(token) 164 | 165 | exit_if_user_exists(dbc, session["name"]) 166 | 167 | else: 168 | exit_if_user_exists(dbc, args[""]) 169 | 170 | if args["--password"] is None: 171 | pwd = getpass() 172 | else: 173 | pwd = args["--password"] 174 | 175 | session = app.auth.get_mobile_session(args[""], pwd) 176 | 177 | 178 | dbc.execute("insert into sessions (user, key) values (?, ?)", (session["name"], session["key"])) 179 | print("User {} added.".format(session["name"])) 180 | 181 | 182 | def cmd_list_users(app, dbc, args): 183 | """ 184 | List all known users. 185 | 186 | usage: scrobbler list-users 187 | """ 188 | 189 | dbc.execute("select * from sessions") 190 | for (user, key) in dbc.fetchall(): 191 | print("{}\t\t| {}".format(user, key)) 192 | print() 193 | 194 | 195 | def cmd_remove_user(app, dbc, args): 196 | """ 197 | Remove a user from the list of known users. 198 | 199 | usage: scrobbler remove-user 200 | """ 201 | 202 | exit_if_user_not_exists(dbc, args[""]) 203 | dbc.execute("delete from sessions where user == ?", (args[""],)) 204 | print("User {} removed.".format(args[""])) 205 | 206 | 207 | def cmd_scrobble(app, dbc, args): 208 | """ 209 | Scrobble a track. 210 | 211 | usage: scrobbler scrobble [--album=] [--duration=] [--time-format=] 212 | [--]