v0.1
This commit is contained in:
188
.gitignore
vendored
Normal file
188
.gitignore
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
cover.jpg
|
||||
### https://raw.github.com/github/gitignore/96d68766538413194aabd55e3622734cd501e715/Python.gitignore
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
|
||||
### https://raw.github.com/github/gitignore/96d68766538413194aabd55e3622734cd501e715/Global/macOS.gitignore
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
|
||||
15
Pipfile
Normal file
15
Pipfile
Normal file
@@ -0,0 +1,15 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
requests = "*"
|
||||
pyobjc-core = "*"
|
||||
pyobjc-framework-usernotifications = "*"
|
||||
pyobjc-framework-cocoa = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
94
Pipfile.lock
generated
Normal file
94
Pipfile.lock
generated
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "fcc7ba477fb286147123b972c7be550af954881ed65de1175fcc7acf49af7fc0"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.9"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
|
||||
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
|
||||
],
|
||||
"version": "==2021.10.8"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721",
|
||||
"sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.9"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==3.3"
|
||||
},
|
||||
"pyobjc-core": {
|
||||
"hashes": [
|
||||
"sha256:43f251881fe36dcb9ca9f7946ab13ddb4aa2499bde3c8bf2dc8144b5fe884629",
|
||||
"sha256:781cfa7a3f72ce7213b4c341b5c552cd63b6664b3b444a23fcb1d5fdd417aa8b",
|
||||
"sha256:920914a1a4cd4f8a54babe93f0b4e4cd93b51d11ae38d8ef17f5edb603b7a949",
|
||||
"sha256:951e716ad30bea52190f5e82604ee2c7f3dc6c04722cf0a3311ad7e4e6af5134",
|
||||
"sha256:b6662805d288cd1bfd257565ddc07541d0162b3167af15e39f7a654b94f6667b",
|
||||
"sha256:ddd9b964df292fa0bbd0d0694befe4c524977780e3f11221ac7e8683d5e9590f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==8.1"
|
||||
},
|
||||
"pyobjc-framework-cocoa": {
|
||||
"hashes": [
|
||||
"sha256:0b0aee74f6c32103338e3735f308052f4cc534c25762130303638aaa6d62d851",
|
||||
"sha256:626f6d084a537b365c0786674036643695e7829ebaf7889aa9b327f3b897a800",
|
||||
"sha256:8075f6cf8cac804d9ed05355c68a8a0cd3ce31a310469279c6c7e49dad47b010",
|
||||
"sha256:9de42dc3e0911d6e30cf41bb4baf9aaf2723a27053f7edbd8ad7d758cf41b81d",
|
||||
"sha256:9e47a2709b8458422752efccaa89f4b52cc8e01345ec71c43c713a891f084bb6",
|
||||
"sha256:b588a3ed9adcdbfafcbfa77bbd72844afb8d987a70cce7a4f3a35a9474271a11"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==8.1"
|
||||
},
|
||||
"pyobjc-framework-usernotifications": {
|
||||
"hashes": [
|
||||
"sha256:249dbb0c05040ee10f68381ff63ec449b57fb45d4b62c6ca8f7c13cd85c9276f",
|
||||
"sha256:9cf6f1fec39ac25d4a8adaa78b9e614ae3f11f13edf5fc2b7002ad2d7452b835",
|
||||
"sha256:c9f77dd911ca54e157fd7d1b5b3273a9c54fcb7e396cda5acf0c25d8be6c7a9a",
|
||||
"sha256:e3f0df0f3015e940c1c963e7803251ddc4f08d8041d471225397f088d8421cee"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==8.1"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
|
||||
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.26.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
|
||||
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==1.26.7"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
||||
0
nporadio2/__init__.py
Normal file
0
nporadio2/__init__.py
Normal file
50
nporadio2/__main__.py
Normal file
50
nporadio2/__main__.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import sys
|
||||
import requests
|
||||
|
||||
from time import sleep
|
||||
|
||||
|
||||
from radiotrack import RadioTrack
|
||||
from notifications import SongNotification
|
||||
|
||||
|
||||
def get_current_song():
|
||||
result = requests.get(
|
||||
"https://www.nporadio2.nl/api/miniplayer/info?channel=npo-radio-2"
|
||||
)
|
||||
|
||||
data = result.json()
|
||||
radio_tracks = data["data"]["radio_track_plays"]["data"][0]["radio_tracks"]
|
||||
track = RadioTrack(radio_tracks)
|
||||
return track
|
||||
|
||||
|
||||
def putchar(char):
|
||||
print(char, end="")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
un = SongNotification()
|
||||
previous = None
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
track = get_current_song()
|
||||
except requests.exceptions.SSLError:
|
||||
putchar(",")
|
||||
continue
|
||||
|
||||
if previous != track:
|
||||
print(f"\nNew track: {track}")
|
||||
track.save_image()
|
||||
un.notify_song(track)
|
||||
else:
|
||||
putchar(".")
|
||||
|
||||
previous = track
|
||||
|
||||
sleep(10)
|
||||
except KeyboardInterrupt:
|
||||
print("\nBye!")
|
||||
77
nporadio2/notifications.py
Normal file
77
nporadio2/notifications.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from UserNotifications import (
|
||||
UNAuthorizationOptionAlert,
|
||||
UNMutableNotificationContent,
|
||||
UNNotificationAttachment,
|
||||
UNNotificationRequest,
|
||||
UNUserNotificationCenter,
|
||||
)
|
||||
from Foundation import NSURL
|
||||
from radiotrack import RadioTrack
|
||||
|
||||
|
||||
class UserNotification:
|
||||
def __init__(self):
|
||||
self._center = UNUserNotificationCenter.currentNotificationCenter()
|
||||
self._center.requestAuthorizationWithOptions_completionHandler_(
|
||||
UNAuthorizationOptionAlert, self._auth_callback
|
||||
)
|
||||
|
||||
def create_notification(
|
||||
self, title=None, subtitle=None, body=None, attachments=None
|
||||
):
|
||||
content = UNMutableNotificationContent.alloc().init()
|
||||
content.setTitle_(title)
|
||||
content.setSubtitle_(subtitle)
|
||||
content.setBody_(body)
|
||||
|
||||
if attachments:
|
||||
content.setAttachments_(attachments)
|
||||
|
||||
request = UNNotificationRequest.requestWithIdentifier_content_trigger_(
|
||||
"top2000_id", content, None
|
||||
)
|
||||
|
||||
self._center.addNotificationRequest_withCompletionHandler_(
|
||||
request, self._notification_callback
|
||||
)
|
||||
|
||||
content.dealloc()
|
||||
|
||||
@staticmethod
|
||||
def _auth_callback(granted, err):
|
||||
if not granted:
|
||||
print(granted, err)
|
||||
|
||||
@staticmethod
|
||||
def _notification_callback(err):
|
||||
if err:
|
||||
print(err)
|
||||
|
||||
|
||||
class SongNotification(UserNotification):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def notify_song(self, song: RadioTrack):
|
||||
|
||||
url = NSURL.URLWithString_(f"file://{song.cover_path}")
|
||||
|
||||
err = None
|
||||
atachement = (
|
||||
UNNotificationAttachment.attachmentWithIdentifier_URL_options_error_(
|
||||
"",
|
||||
url,
|
||||
None,
|
||||
err,
|
||||
)
|
||||
)[0]
|
||||
|
||||
if err:
|
||||
print(err)
|
||||
atachement = None
|
||||
|
||||
self.create_notification(
|
||||
song.title,
|
||||
song.artist,
|
||||
attachments=[atachement],
|
||||
)
|
||||
30
nporadio2/radiotrack.py
Normal file
30
nporadio2/radiotrack.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from typing import Optional
|
||||
import requests
|
||||
import tempfile
|
||||
|
||||
|
||||
class RadioTrack:
|
||||
def __init__(self, data: dict) -> None:
|
||||
self.artist = data["artist"]
|
||||
self.title = data["name"]
|
||||
self.cover = data["cover_url"]
|
||||
self.cover_path: Optional[str] = None
|
||||
|
||||
def save_image(self):
|
||||
r = requests.get(self.cover)
|
||||
|
||||
if r.status_code == 200:
|
||||
with tempfile.NamedTemporaryFile("wb", suffix=".jpg", delete=False) as f:
|
||||
f.write(r.content)
|
||||
self.cover_path = f.name
|
||||
else:
|
||||
print(r.status_code)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.artist} - {self.title}"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, RadioTrack):
|
||||
return self.title == other.title and self.artist == other.artist
|
||||
|
||||
return False
|
||||
Reference in New Issue
Block a user