├── 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 | [--]