added podcast script

This commit is contained in:
Alexander Karpov 2023-02-20 13:56:01 +03:00
commit b81681b9d7
5 changed files with 391 additions and 0 deletions

229
.gitignore vendored Normal file
View File

@ -0,0 +1,229 @@
.idea
.env
### Python template
# 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/
*.egg-info/
.installed.cfg
*.egg
# 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/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
staticfiles/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# Environments
.venv
venv/
ENV/
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Provided default Pycharm Run/Debug Configurations should be tracked by git
# In case of local modifications made by Pycharm, use update-index command
# for each changed file, like this:
# git update-index --assume-unchanged .idea/akarpov.iml
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Windows template
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db

4
podcasts/.env.template Normal file
View File

@ -0,0 +1,4 @@
YANDEX_TOKEN=
CHAT_ID=
BOT_TOKEN=
TELEGRAM_SERVER=

28
podcasts/README.md Normal file
View File

@ -0,0 +1,28 @@
# Podcast loader script
Script to load current listening track from yandex music and send it to telegram chat.
### Configuration
Obtain yandex music token - https://music-yandex-bot.ru/
Obtain telegram api id and hash for local telegram image - https://my.telegram.org/
### Installation
OPTIONAL: start local telegram bot server for file upload
```shell
$ docker run -d -p 8081:8081 --name=telegram-bot-api --restart=always -v telegram-bot-api-data:/var/lib/telegram-bot-api -e TELEGRAM_API_ID=<api_id> -e TELEGRAM_API_HASH=<api-hash> aiogram/telegram-bot-api:latest
```
```shell
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install -r requirement.txt
```
### Run
program runs via python-daemon
```shell
$ python3 podcasts.py
```
Note: can be modified to send all(unique) tracks, just remove ```if "podcast" in last_track.type``` check

107
podcasts/podcasts.py Normal file
View File

@ -0,0 +1,107 @@
import asyncio
import os
import daemon
from io import BytesIO
from time import sleep
from aiogram import Bot
from aiogram.bot.api import TelegramAPIServer
from mutagen.easyid3 import EasyID3
from mutagen.mp3 import MP3
from mutagen.id3 import APIC, ID3, TORY
from pydub import AudioSegment
from yandex_music import Client, Track
from dotenv import load_dotenv
load_dotenv(dotenv_path=".env")
YANDEX_TOKEN = os.getenv("YANDEX_TOKEN")
CHAT_ID = os.getenv("CHAT_ID")
TOKEN = os.getenv("BOT_TOKEN")
TELEGRAM_SERVER = os.getenv("TELEGRAM_SERVER", default=None)
if TELEGRAM_SERVER:
local_server = TelegramAPIServer.from_base(TELEGRAM_SERVER)
bot = Bot(TOKEN, server=local_server)
else:
bot = Bot(TOKEN)
client = Client(YANDEX_TOKEN).init()
latest_podcast = None
latest_sent = True
podcasts_listened = []
with daemon.DaemonContext():
while True:
try:
queues = client.queues_list()
last_queue = client.queue(queues[0].id)
last_track_id = last_queue.get_current_track()
last_track: Track = last_track_id.fetch_track()
if "podcast" in last_track.type:
if last_track_id not in podcasts_listened:
if last_track_id == latest_podcast and not latest_sent:
latest_sent = True
podcasts_listened.append(last_track_id)
title = last_track.title
album = last_track.albums[0]
url = f"https://music.yandex.ru/track/{last_track.id}"
desc = last_track.short_description.split("\n")[0]
last_track.download_cover(filename="cover.png")
img_path = os.path.abspath("cover.png")
last_track.download(filename="file", codec="mp3")
orig_path = os.path.abspath("file")
path = os.path.abspath("file.mp3")
AudioSegment.from_file(orig_path).export(path)
os.remove(orig_path)
# set music meta
tag = MP3(path, ID3=ID3)
tag.tags.add(
APIC(
encoding=3, # 3 is for utf-8
mime="image/png", # image/jpeg or image/png
type=3, # 3 is for the cover image
desc="Cover",
data=open(img_path, "rb").read(),
)
)
tag.tags.add(TORY(text=str(album.year)))
tag.save()
tag = EasyID3(path)
tag["title"] = title
tag["album"] = album.title
tag.save()
with open(path, "rb") as tmp:
obj = BytesIO(tmp.read())
obj.name = f"{title}.mp3"
loop = asyncio.get_event_loop()
coroutine = bot.send_audio(
chat_id=CHAT_ID,
audio=obj,
caption=f"{title} - {album.title}\n{desc}\n\n{url}",
title=title,
performer=album.title,
)
loop.run_until_complete(coroutine)
else:
latest_podcast = last_track_id
latest_sent = False
except BaseException as e:
loop = asyncio.get_event_loop()
coroutine = bot.send_message(CHAT_ID, text=str(e))
loop.run_until_complete(coroutine)
sleep(5 * 60)

23
podcasts/requirement.txt Normal file
View File

@ -0,0 +1,23 @@
aiofiles==23.1.0
aiogram==2.25.1
aiohttp==3.8.4
aiosignal==1.3.1
async-timeout==4.0.2
attrs==22.2.0
Babel==2.9.1
certifi==2022.12.7
charset-normalizer==3.0.1
frozenlist==1.3.3
idna==3.4
magic-filter==1.0.9
multidict==6.0.4
mutagen==1.45.1
pydub==0.25.1
PySocks==1.7.1
python-dotenv==0.21.1
pytz==2022.7.1
requests==2.28.2
urllib3==1.26.14
yandex-music==2.0.1
yarl==1.8.2
python-daemon==2.3.2