added anime scripts and new animebyter
This commit is contained in:
parent
ddbb5b106b
commit
9854acd343
37
Animebyter/Animebyter.py
Normal file
37
Animebyter/Animebyter.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from aiohttp import ClientSession
|
||||||
|
from feedparser import parse
|
||||||
|
from os import getenv
|
||||||
|
from json import dumps
|
||||||
|
import logging
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
web = ClientSession()
|
||||||
|
|
||||||
|
class Anime:
|
||||||
|
def __eq__(self,other):
|
||||||
|
return self.title == other.title
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.title)
|
||||||
|
|
||||||
|
def __init__(self,name,le,tl,res):
|
||||||
|
self.title = name.replace("/","-")
|
||||||
|
self.last_episode = le
|
||||||
|
self.torrent_link = tl
|
||||||
|
self.resolution = res.strip()
|
||||||
|
self.id = str(int(hashlib.sha256(self.title.encode('utf-8')).hexdigest(), 16) % 10**8)
|
||||||
|
|
||||||
|
async def get_airing():
|
||||||
|
r = []
|
||||||
|
async with web.get("https://animebytes.tv/feed/rss_torrents_airing_anime/{}".format(getenv("ab_key"))) as res:
|
||||||
|
if res.status==200:
|
||||||
|
txt = await res.text()
|
||||||
|
rss = parse(txt)
|
||||||
|
for i in rss['entries']:
|
||||||
|
try:
|
||||||
|
title = i['ab_grouptitle']
|
||||||
|
ep = int((''.join(x for x in i['ab_torrentproperty'].split("|")[6] if x.isdigit())).strip())
|
||||||
|
link = i['link']
|
||||||
|
r.append(Anime(title,ep,link,i['ab_torrentproperty'].split("|")[3]))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(str(e))
|
||||||
|
return r
|
@ -5,10 +5,14 @@ RUN apk add libffi-dev
|
|||||||
RUN apk add openssl-dev
|
RUN apk add openssl-dev
|
||||||
RUN apk add python3-dev
|
RUN apk add python3-dev
|
||||||
|
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
RUN pip install -r /app/requirements.txt
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=0
|
||||||
|
ENV interval 300
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV interval 300
|
|
||||||
|
|
||||||
RUN pip install -r requirements.txt
|
CMD python3 main.py
|
||||||
|
|
||||||
CMD python3 animebyter.py
|
|
||||||
|
115
Animebyter/Downloader.py
Normal file
115
Animebyter/Downloader.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
from Animebyter import get_airing
|
||||||
|
from asyncio import sleep, Queue, get_event_loop
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from pickledb import PickleDB
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
INTERVAL = int(os.getenv("interval","5"))
|
||||||
|
web = ClientSession()
|
||||||
|
QB_URL = os.getenv("qbit_url")
|
||||||
|
store = PickleDB(os.getenv("database"), True, False)
|
||||||
|
dl_queue = Queue()
|
||||||
|
loop = get_event_loop()
|
||||||
|
|
||||||
|
if not store.exists("watching"):
|
||||||
|
store.lcreate("watching")
|
||||||
|
if not store.exists("qbUser"):
|
||||||
|
store.set("qbUser", "")
|
||||||
|
if not store.exists("qbPass"):
|
||||||
|
store.set("qbPass", "")
|
||||||
|
|
||||||
|
class qbLoginException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NotLoggedInException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DownloadableItem:
|
||||||
|
def __init__(self,anime):
|
||||||
|
self.anime = anime
|
||||||
|
|
||||||
|
def complete(self):
|
||||||
|
watching = store.get("watching")
|
||||||
|
for v in watching:
|
||||||
|
if v['id'] == self.anime.id:
|
||||||
|
store.lremvalue("watching",v)
|
||||||
|
v["last_episode"] = self.anime.last_episode
|
||||||
|
store.ladd("watching",v)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def login_qb(username=store.get("qbUser"),password=store.get("qbPass"), client=web):
|
||||||
|
async with client.post(QB_URL+'/login',data={'username':username,'password':password}) as res:
|
||||||
|
if res.status!=200:
|
||||||
|
raise qbLoginException(await res.text())
|
||||||
|
else:
|
||||||
|
logging.info("Logged into qBittorrent")
|
||||||
|
|
||||||
|
async def add_anime_torrent(anime):
|
||||||
|
logging.info("Adding episode {} of {}".format(anime.last_episode,anime.title))
|
||||||
|
path = os.path.join(store.get('downloadPath'),anime.title)
|
||||||
|
async with web.post(QB_URL+'/command/download',data={'urls':anime.torrent_link,'savepath':path,'category':store.get("downloadLabel")}) as res:
|
||||||
|
if res.status==200:
|
||||||
|
return 1
|
||||||
|
elif res.status==403:
|
||||||
|
raise NotLoggedInException()
|
||||||
|
else:
|
||||||
|
raise Exception(await res.text())
|
||||||
|
|
||||||
|
async def get_last_added():
|
||||||
|
async with web.get(QB_URL+"/query/torrents",params={"category":"Anime","sort":"added_on","reverse":"true"}) as res:
|
||||||
|
if res.status==200:
|
||||||
|
res = await res.json()
|
||||||
|
return res[0]
|
||||||
|
|
||||||
|
from Notifications import downloading
|
||||||
|
async def add_to_download_list(anime):
|
||||||
|
last = await get_last_added()
|
||||||
|
if last:
|
||||||
|
downloading[last["hash"]] = anime
|
||||||
|
|
||||||
|
async def downloader():
|
||||||
|
logging.info("Starting downloader")
|
||||||
|
while True:
|
||||||
|
item = await dl_queue.get()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await add_anime_torrent(item.anime)
|
||||||
|
await add_to_download_list(item.anime)
|
||||||
|
item.complete()
|
||||||
|
logging.info("Added episode {} of {}".format(item.anime.last_episode,item.anime.title))
|
||||||
|
break
|
||||||
|
except NotLoggedInException:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await login_qb()
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logging.warn("Could not log into qBittorrent ({})".format(str(e)))
|
||||||
|
await sleep(5)
|
||||||
|
continue
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(str(e))
|
||||||
|
await sleep(3)
|
||||||
|
|
||||||
|
async def checker():
|
||||||
|
logging.info("Starting new episode checker")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
logging.debug("Checking for new episodes")
|
||||||
|
airing = await get_airing()
|
||||||
|
watching = store.get("watching")
|
||||||
|
for air in airing:
|
||||||
|
for watch in watching:
|
||||||
|
if air.id == watch['id'] and air.resolution == watch["resolution"]:
|
||||||
|
if air.last_episode > watch['last_episode']:
|
||||||
|
logging.debug("Attempting to add episode {} of {}".format(air.last_episode,air.title))
|
||||||
|
item = DownloadableItem(air)
|
||||||
|
await dl_queue.put(item)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(str(e))
|
||||||
|
continue
|
||||||
|
finally:
|
||||||
|
await sleep(INTERVAL)
|
49
Animebyter/Notifications.py
Normal file
49
Animebyter/Notifications.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from aiohttp import ClientSession
|
||||||
|
from os import getenv
|
||||||
|
from Downloader import QB_URL, login_qb, web
|
||||||
|
from asyncio import sleep
|
||||||
|
import logging
|
||||||
|
|
||||||
|
notif_web = ClientSession()
|
||||||
|
URL = getenv("gotify_url")
|
||||||
|
downloading = {}
|
||||||
|
|
||||||
|
async def _send_notification(title,message):
|
||||||
|
if not URL:
|
||||||
|
logging.debug("Ignoring notification push because gotify url not set")
|
||||||
|
return
|
||||||
|
res = await notif_web.post(URL, data={
|
||||||
|
"title": title,
|
||||||
|
"message": message
|
||||||
|
})
|
||||||
|
if res.status == 200:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
logging.warn("Could not push notification ({}: {})".format(res.status, await res.text()))
|
||||||
|
|
||||||
|
async def send_anime_notification(anime):
|
||||||
|
logging.info("Finished downloading episode {} of {}".format(anime.last_episode, anime.title))
|
||||||
|
title = anime.title
|
||||||
|
message = "Episode {} has finished downloading".format(anime.last_episode)
|
||||||
|
return await _send_notification(title, message)
|
||||||
|
|
||||||
|
async def dl_watchdog():
|
||||||
|
await login_qb(client=web)
|
||||||
|
logging.info("Starting download watchdog")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
res = await web.get(QB_URL+"/query/torrents",params={'filter':'downloading', 'category':'Anime'})
|
||||||
|
if res.status==200:
|
||||||
|
res = await res.json()
|
||||||
|
hashes = [i['hash'] for i in res]
|
||||||
|
for i in downloading:
|
||||||
|
if i not in hashes:
|
||||||
|
anime = downloading.pop(i,None)
|
||||||
|
await send_anime_notification(anime)
|
||||||
|
else:
|
||||||
|
logging.warn("Something went wrong with fetching downloads ({}: {})".format(res.status,await res.text()))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(str(e))
|
||||||
|
continue
|
||||||
|
finally:
|
||||||
|
await sleep(5)
|
34
Animebyter/README.md
Normal file
34
Animebyter/README.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Animebyter
|
||||||
|
|
||||||
|
##### A daemon and web app that keeps track of the shows you watch and automatically downloads new episode off Animebytes.tv
|
||||||
|
----
|
||||||
|
|
||||||
|
### How to set-up
|
||||||
|
0) Set-up docker. This guide assumes that you use qBittorrent as a docker container and that you have some sort of web server with proxying capabilities, also dockerized.
|
||||||
|
1) Edit docker-compose:
|
||||||
|
- Edit the `qbit-network` external name to the one that matches your qbittorrent network.
|
||||||
|
- Edit the `qbit_url` enironment variable to the hostname of your qbittorrent server. This will typically be your container name (so make sure to set one).
|
||||||
|
- Edit the `ab_key` environment variable with your animebytes passkey. You can find this under Settings > Account > Passkey.
|
||||||
|
- Edit the `base_url` environment variable with the path that you've set on your reverse proxy. It should be `/` if you run it on a subdomain.
|
||||||
|
- Edit the `gotify_url` to a valid Gotify server URL if you want to receive notifications.
|
||||||
|
2) Run `docker-compose up -d`. If you've set-up your docker-compose correctly it should run without problems.
|
||||||
|
3) Configure your reverse proxy. The docker-compose.yml provided, exposes a network called `animebyter-network` and names the container `animebyter`. You should edit the deployment of your web-server, making sure it is connected to the `animebyter-network`. The app should then be accessible on `animebyter:5000`. Here's a sample configuration for the caddy webserver:
|
||||||
|
```
|
||||||
|
proxy /animebyter animebyter:5000 {
|
||||||
|
transparent
|
||||||
|
without /animebyter
|
||||||
|
}
|
||||||
|
redir /animebyter /animebyter/
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to use
|
||||||
|
The page shows two tables: Airing and Watching. You can add any show by clicking the (+) and remove it by clicking (-). Make sure to set a download path and preferably a label also. Also make sure to log into qBittorrent.
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Want to contribute? Sure
|
||||||
|
|
||||||
|
License
|
||||||
|
----
|
||||||
|
MIT
|
@ -1,252 +0,0 @@
|
|||||||
from feedparser import parse
|
|
||||||
from aiohttp import ClientSession
|
|
||||||
from discord.ext.commands import Bot
|
|
||||||
from tinydb import TinyDB, Query
|
|
||||||
from os.path import join
|
|
||||||
from os import getenv
|
|
||||||
from asyncio import sleep
|
|
||||||
from traceback import print_exc
|
|
||||||
|
|
||||||
client = Bot('ab!')
|
|
||||||
QB_URL = getenv("qbit_url")
|
|
||||||
INTERVAL = int(getenv("INTERVAL")) if getenv("INTERVAL") else 300
|
|
||||||
web = ClientSession()
|
|
||||||
db = TinyDB(getenv("database_path","animebyter.json"))
|
|
||||||
path = getenv("download_path")
|
|
||||||
|
|
||||||
def get_channel_id():
|
|
||||||
res = db.search(Query().type == 'channel')
|
|
||||||
if len(res) > 0:
|
|
||||||
chn = res[0]['id']
|
|
||||||
else:
|
|
||||||
db.insert({'type':'channel','id':''})
|
|
||||||
chn = None
|
|
||||||
return chn
|
|
||||||
|
|
||||||
def get_notif_id():
|
|
||||||
res = db.search(Query().type == 'notifications')
|
|
||||||
if len(res) > 0:
|
|
||||||
id = res[0]['id']
|
|
||||||
else:
|
|
||||||
db.insert({'type':'notifications','id':''})
|
|
||||||
id = None
|
|
||||||
return id
|
|
||||||
|
|
||||||
get_channel_id()
|
|
||||||
get_notif_id()
|
|
||||||
|
|
||||||
downloading = []
|
|
||||||
|
|
||||||
class Anime:
|
|
||||||
def __init__(self,name,le,tl,res):
|
|
||||||
self.title = name.replace("/","-")
|
|
||||||
self.last_episode = le
|
|
||||||
self.torrent_link = tl
|
|
||||||
self.resolution = res.strip()
|
|
||||||
|
|
||||||
def __eq__(self,other):
|
|
||||||
return self.title == other.title
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self.title)
|
|
||||||
|
|
||||||
async def login_qb():
|
|
||||||
async with web.post(QB_URL+'/login',data={'username':getenv("qbit_user"),'password':getenv("qbit_pass")}) as res:
|
|
||||||
if res.status!=200:
|
|
||||||
print("Could not authenticate with qBittorrent. Exiting...")
|
|
||||||
exit(1)
|
|
||||||
else:
|
|
||||||
print("Logged in to qBittorrent")
|
|
||||||
|
|
||||||
async def get_airing():
|
|
||||||
r = []
|
|
||||||
res = await web.get("https://animebytes.tv/feed/rss_torrents_airing_anime/{}".format(getenv("ab_key")))
|
|
||||||
if res.status==200:
|
|
||||||
txt = await res.text()
|
|
||||||
rss = parse(txt)
|
|
||||||
for i in rss['entries']:
|
|
||||||
try:
|
|
||||||
title = i['ab_grouptitle']
|
|
||||||
ep = int((''.join(x for x in i['ab_torrentproperty'].split("|")[6] if x.isdigit())).strip())
|
|
||||||
link = i['link']
|
|
||||||
r.append(Anime(title,ep,link,i['ab_torrentproperty'].split("|")[3]))
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
return r
|
|
||||||
|
|
||||||
async def add_anime(anime):
|
|
||||||
print("Adding episode {} of {}".format(anime.last_episode,anime.title))
|
|
||||||
try:
|
|
||||||
await add_torrent(anime.torrent_link, join(path,anime.title), 'Anime')
|
|
||||||
except Exception as e:
|
|
||||||
print("Failed to add episode {} of {} ({})".format(anime.last_episode,anime.title,e))
|
|
||||||
else:
|
|
||||||
msg = "Added episode {} of {}".format(anime.last_episode,anime.title)
|
|
||||||
print(msg)
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
async def add_torrent(url,path,category):
|
|
||||||
async with web.post(QB_URL+'/command/download',data={'urls':url,'savepath':path,'category':category}) as res:
|
|
||||||
if res.status==200:
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
raise Exception(await res.text())
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
print("Starting new episode checker")
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
print("Checking for new episodes")
|
|
||||||
airing = await get_airing()
|
|
||||||
for i in airing:
|
|
||||||
res = db.search(Query().title==i.title)
|
|
||||||
if res:
|
|
||||||
le = res[0]['last_episode']
|
|
||||||
if le<i.last_episode and i.resolution in ("1080p"):
|
|
||||||
msg = await add_anime(i)
|
|
||||||
if msg:
|
|
||||||
await client.get_channel(get_channel_id()).send(msg)
|
|
||||||
db.update({'last_episode':i.last_episode},Query().title==i.title)
|
|
||||||
except Exception as e:
|
|
||||||
print(str(e))
|
|
||||||
print_exc()
|
|
||||||
continue
|
|
||||||
finally:
|
|
||||||
await sleep(INTERVAL)
|
|
||||||
|
|
||||||
async def dl_watchdog():
|
|
||||||
print("Starting download watchdog")
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
res = await web.get(QB_URL+"/query/torrents",params={'filter':'downloading'})
|
|
||||||
if res.status==200:
|
|
||||||
res = await res.json()
|
|
||||||
names = []
|
|
||||||
for i in res:
|
|
||||||
names.append(i['name'])
|
|
||||||
if i['name'] not in downloading:
|
|
||||||
downloading.append(i['name'])
|
|
||||||
for i in downloading:
|
|
||||||
if i not in names:
|
|
||||||
try:
|
|
||||||
downloading.remove(i)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
await client.get_channel(get_channel_id()).send(":exclamation: <@!{}> {} has finished downloading.".format(get_notif_id(),i))
|
|
||||||
else:
|
|
||||||
print("Something went wrong with fetching downloads ({}: {})".format(res.status,await res.text()))
|
|
||||||
except Exception as e:
|
|
||||||
print(str(e))
|
|
||||||
print_exc()
|
|
||||||
continue
|
|
||||||
finally:
|
|
||||||
await sleep(10)
|
|
||||||
|
|
||||||
def chunks(s, n=1999):
|
|
||||||
for start in range(0, len(s), n):
|
|
||||||
yield s[start:start+n]
|
|
||||||
|
|
||||||
@client.command(pass_context=True)
|
|
||||||
async def add(ctx):
|
|
||||||
airing = list(set(await get_airing()))
|
|
||||||
txt = ""
|
|
||||||
for i,v in enumerate(airing):
|
|
||||||
txt+="{}) {}\n".format(i,v.title)
|
|
||||||
msgs = []
|
|
||||||
for i in chunks(txt):
|
|
||||||
msgs.append(await ctx.send(i))
|
|
||||||
msg = await client.wait_for('message',check=lambda m: m.author==ctx.author and m.channel==ctx.channel)
|
|
||||||
await ctx.channel.delete_messages(msgs)
|
|
||||||
if msg:
|
|
||||||
try:
|
|
||||||
msg = int(msg.content)
|
|
||||||
except Exception as e:
|
|
||||||
return await ctx.send(e)
|
|
||||||
if msg>=len(airing):
|
|
||||||
return await ctx.send("Invalid number")
|
|
||||||
an = airing[msg]
|
|
||||||
db.insert({'title':an.title,'last_episode':an.last_episode-1})
|
|
||||||
return await ctx.send("Added {}".format(an.title))
|
|
||||||
|
|
||||||
|
|
||||||
@client.command(pass_context=True)
|
|
||||||
async def remove(ctx):
|
|
||||||
watching = [i for i in db.all() if 'title' in i]
|
|
||||||
txt = ""
|
|
||||||
for i,v in enumerate(watching):
|
|
||||||
txt+="{}) {}\n".format(i,v['title'])
|
|
||||||
msgs = []
|
|
||||||
for i in chunks(txt):
|
|
||||||
msgs.append(await ctx.send(i))
|
|
||||||
msg = await client.wait_for('message',check=lambda m: m.author==ctx.author and m.channel==ctx.channel)
|
|
||||||
if len(msgs)==1:
|
|
||||||
await msgs[0].delete()
|
|
||||||
else:
|
|
||||||
await ctx.channel.delete_messages(msgs)
|
|
||||||
if msg:
|
|
||||||
try:
|
|
||||||
msg = int(msg.content)
|
|
||||||
except Exception as e:
|
|
||||||
return await ctx.send(e)
|
|
||||||
if msg>=len(watching):
|
|
||||||
return await ctx.send("Invalid number")
|
|
||||||
an = watching[msg]
|
|
||||||
db.remove(Query().title==an['title'])
|
|
||||||
return await ctx.send("Removed {}".format(an['title']))
|
|
||||||
|
|
||||||
@client.command(pass_context=True)
|
|
||||||
async def down(ctx):
|
|
||||||
airing = await get_airing()
|
|
||||||
txt = ""
|
|
||||||
for i,v in enumerate(airing):
|
|
||||||
txt+="{}) {} (Episode: {})[{}]\n".format(i,v.title,v.last_episode,v.resolution)
|
|
||||||
msgs = []
|
|
||||||
for i in chunks(txt):
|
|
||||||
msgs.append(await ctx.send(i))
|
|
||||||
msg = await client.wait_for('message',check=lambda m: m.author==ctx.author and m.channel==ctx.channel)
|
|
||||||
print(msg)
|
|
||||||
await ctx.channel.delete_messages(msgs)
|
|
||||||
if msg:
|
|
||||||
try:
|
|
||||||
msg = int(msg.content)
|
|
||||||
except Exception as e:
|
|
||||||
return await ctx.send(e)
|
|
||||||
if msg>=len(airing):
|
|
||||||
return await ctx.send("Invalid number")
|
|
||||||
await ctx.send(await add_anime(airing[msg]))
|
|
||||||
|
|
||||||
@client.command(pass_context=True)
|
|
||||||
async def torrent(ctx,url,path=None,category=None):
|
|
||||||
if not path:
|
|
||||||
if 'jpopsuki' in url:
|
|
||||||
path = '/Music/'
|
|
||||||
else:
|
|
||||||
path = '/Downloads/'
|
|
||||||
if not category:
|
|
||||||
if 'jpopsuki' in url:
|
|
||||||
category = 'Music'
|
|
||||||
try:
|
|
||||||
await ctx.send(await add_torrent(url,path,category))
|
|
||||||
except Exception as e:
|
|
||||||
await ctx.send("Failed to add torrent: {}".format(e))
|
|
||||||
|
|
||||||
|
|
||||||
@client.command(pass_context=True)
|
|
||||||
async def setchannel(ctx):
|
|
||||||
db.update({'id':ctx.channel.id},Query().type == 'channel')
|
|
||||||
await ctx.send("I will now send notifications to this channel!")
|
|
||||||
|
|
||||||
@client.command(pass_context=True)
|
|
||||||
async def setnotif(ctx):
|
|
||||||
db.update({'id':ctx.author.id}, Query().type == 'notifications')
|
|
||||||
await ctx.send("I will now tag you for notifications!")
|
|
||||||
|
|
||||||
@client.event
|
|
||||||
async def on_ready():
|
|
||||||
print("Starting animebyter")
|
|
||||||
await login_qb()
|
|
||||||
client.loop.create_task(main())
|
|
||||||
client.loop.create_task(dl_watchdog())
|
|
||||||
|
|
||||||
client.run(getenv("discord_token"))
|
|
@ -1,21 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Adds new episodes from animebytes on qbittorrent
|
|
||||||
After=network.target qbittorrent.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
|
|
||||||
WorkingDirectory=/home/marios/Animebyter #The location of the animebyter script
|
|
||||||
ExecStart=/usr/bin/python3.7 animebyter.py
|
|
||||||
User=marios
|
|
||||||
Restart=always
|
|
||||||
|
|
||||||
Environment=qbit_url=http://127.0.0.1:8080 #URL of your qBittorrent WebUI
|
|
||||||
Environment=qbit_user=admin #Username for qBittorrent WebUI
|
|
||||||
Environment=qbit_pass=adminadmin #Password for qBittorrent WebUI
|
|
||||||
Environment=ab_key=YOUR_ANIMEBYTES_TOKEN #Your animebytes token. You can find this by querying any of the RSS feeds
|
|
||||||
Environment=channel=THE_CHANNEL_YOU_WANT_TO_SEND_NOTIFICATIONS_IN #The ID of the channel you want to send notifications in on discord
|
|
||||||
Environment=discord_token=YOUR_DISCORD_TOKEN #The token of your discord bot
|
|
||||||
Environment=INTERVAL=300 #It will look for new episodes every x seconds
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
@ -5,18 +5,22 @@ services:
|
|||||||
container_name: animebyter
|
container_name: animebyter
|
||||||
environment:
|
environment:
|
||||||
- qbit_url=http://qbittorrent:8080
|
- qbit_url=http://qbittorrent:8080
|
||||||
- qbit_user=admin
|
- ab_key=YOUR_ANIMEBYTES_TOKEN
|
||||||
- qbit_pass=adminadmin
|
- database=/db/animebyter.json
|
||||||
- ab_key=YOUR_ANIMEBYTES_KEY
|
- base_url=/animebyter
|
||||||
- discord_token=YOUR_DISCORD_TOKEN
|
- gotify_url=Gotify_URL
|
||||||
- download_path=/Anime
|
- LOGLEVEL=INFO
|
||||||
volumes:
|
volumes:
|
||||||
- ./db:/db
|
- ./db:/db
|
||||||
networks:
|
networks:
|
||||||
- qbit-network
|
- qbit-network
|
||||||
|
- animebyter-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
animebyter-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
qbit-network:
|
qbit-network:
|
||||||
external:
|
external:
|
||||||
name: qbittorrent_qbit-network
|
name: qbittorrent_qbit-network
|
95
Animebyter/main.py
Normal file
95
Animebyter/main.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from quart import Quart, request, make_response, render_template, redirect
|
||||||
|
from Animebyter import get_airing
|
||||||
|
from Downloader import downloader, store, login_qb, qbLoginException, checker
|
||||||
|
from Notifications import dl_watchdog
|
||||||
|
from asyncio import get_event_loop,gather
|
||||||
|
import os
|
||||||
|
from sys import stdout
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
app = Quart(__name__,"/static")
|
||||||
|
base_url = os.getenv("base_url")
|
||||||
|
|
||||||
|
class LastAiring:
|
||||||
|
airing = []
|
||||||
|
def get(self):
|
||||||
|
return self.airing
|
||||||
|
def sett(self,a):
|
||||||
|
self.airing = a
|
||||||
|
last_airing = LastAiring()
|
||||||
|
|
||||||
|
class FakeObj:
|
||||||
|
def __init__(self,dc):
|
||||||
|
for i in dc.keys():
|
||||||
|
setattr(self,i,dc[i])
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def home():
|
||||||
|
airing = await get_airing()
|
||||||
|
last_airing.sett(airing)
|
||||||
|
watching = store.get("watching")
|
||||||
|
dl_path = store.get("downloadPath")
|
||||||
|
dl_label = store.get("downloadLabel")
|
||||||
|
return await render_template('index.html', airing=airing, watching=[FakeObj(i) for i in watching], dl_path=dl_path, dl_label=dl_label)
|
||||||
|
|
||||||
|
@app.route("/addAnime")
|
||||||
|
async def add_show():
|
||||||
|
id = request.args.get("id")
|
||||||
|
la = last_airing.get()
|
||||||
|
show = None
|
||||||
|
for i in la:
|
||||||
|
if i.id == id:
|
||||||
|
show = i
|
||||||
|
break
|
||||||
|
if show:
|
||||||
|
show.last_episode -=1
|
||||||
|
store.ladd("watching",vars(show))
|
||||||
|
return redirect(base_url)
|
||||||
|
else:
|
||||||
|
return await render_template("error.html", message="Show does not exist", base_url=base_url)
|
||||||
|
|
||||||
|
@app.route("/removeAnime")
|
||||||
|
async def remove_show():
|
||||||
|
id = request.args.get("id")
|
||||||
|
watching = store.get("watching")
|
||||||
|
for i in watching:
|
||||||
|
if id == i['id']:
|
||||||
|
store.lremvalue("watching",i)
|
||||||
|
return redirect(base_url)
|
||||||
|
return await render_template("error.html",message="Show does not exist", base_url=base_url)
|
||||||
|
|
||||||
|
@app.route("/updatePath", methods=["POST"])
|
||||||
|
async def set_path():
|
||||||
|
path = (await request.form).get("path")
|
||||||
|
if os.path.isdir(path):
|
||||||
|
store.set("downloadPath", path)
|
||||||
|
return redirect(base_url)
|
||||||
|
else:
|
||||||
|
return await render_template("error.html", message="{} is not a valid path".format(path))
|
||||||
|
|
||||||
|
@app.route("/updateLabel", methods=["POST"])
|
||||||
|
async def set_label():
|
||||||
|
label = (await request.form).get("label")
|
||||||
|
store.set("downloadLabel", label)
|
||||||
|
return redirect(base_url)
|
||||||
|
|
||||||
|
@app.route("/updateCreds", methods=["POST"])
|
||||||
|
async def update_creds():
|
||||||
|
form = await request.form
|
||||||
|
username = form.get("user")
|
||||||
|
password = form.get("password")
|
||||||
|
try:
|
||||||
|
await login_qb(username, password)
|
||||||
|
store.set("qbUser", username)
|
||||||
|
store.set("qbPass", password)
|
||||||
|
return redirect(base_url)
|
||||||
|
except qbLoginException:
|
||||||
|
return await render_template("error.html", message="Invalid credentials. Try again", base_url=base_url)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
server_task = app.run_task("0.0.0.0")
|
||||||
|
loop = get_event_loop()
|
||||||
|
|
||||||
|
loop.run_until_complete(gather(server_task,downloader(),checker(),dl_watchdog()))
|
4
Animebyter/requirements.txt
Normal file
4
Animebyter/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
feedparser
|
||||||
|
quart
|
||||||
|
aiohttp
|
||||||
|
pickledb
|
38
Animebyter/static/css/style.css
Normal file
38
Animebyter/static/css/style.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
table, th, td {
|
||||||
|
border: 2px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar {
|
||||||
|
width: 100%;
|
||||||
|
height: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#watching {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#airing {
|
||||||
|
float:left;
|
||||||
|
margin-right: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
width: 400px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
17
Animebyter/templates/error.html
Normal file
17
Animebyter/templates/error.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Error</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ message }}</h1>
|
||||||
|
<script>
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.href = "{{ base_url }}";
|
||||||
|
},2000)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
71
Animebyter/templates/index.html
Normal file
71
Animebyter/templates/index.html
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
<title>ABdown</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="topbar">
|
||||||
|
<h1>ABdown</h1>
|
||||||
|
<form method="POST" action="updatePath">
|
||||||
|
Download path: <input type="text" name="path" value="{{ dl_path }}">
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="updateLabel">
|
||||||
|
Download label: <input type="text" name="label" value="{{ dl_label }}">
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="updateCreds">
|
||||||
|
Username: <input type="text" name="user" value="{{ qb_user }}">
|
||||||
|
Password: <input type="password" name="pass" value="********">
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="airing">
|
||||||
|
<h2>Airing</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Resolution</th>
|
||||||
|
<th>Last episode</th>
|
||||||
|
<th>Add</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in airing %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ i.title }}</td>
|
||||||
|
<td>{{ i.resolution }}</td>
|
||||||
|
<td>{{ i.last_episode }}</td>
|
||||||
|
<td><a href="addAnime?id={{ i.id }}"><button>+</button></a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="watching">
|
||||||
|
<h2>Watching</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th class="text">Title</th>
|
||||||
|
<th>Resolution</th>
|
||||||
|
<th>Last episode</th>
|
||||||
|
<th>Remove</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in watching %}
|
||||||
|
<tr>
|
||||||
|
<td class="text">{{ i.title }}</td>
|
||||||
|
<td>{{ i.resolution }}</td>
|
||||||
|
<td>{{ i.last_episode }}</td>
|
||||||
|
<td><a href="removeAnime?id={{ i.id }}"><button>-</button></a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -3,18 +3,22 @@ from os import getenv
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import pymysql
|
import pymysql
|
||||||
|
|
||||||
|
|
||||||
class LogItem:
|
class LogItem:
|
||||||
def __init__(self, line):
|
def __init__(self, line):
|
||||||
self.ip = line.split(" ")[0]
|
self.ip = line.split(" ")[0]
|
||||||
self.timestamp = datetime.strptime(line.split("[")[1].split("]")[0].split("+")[0].strip(),"%d/%b/%Y:%H:%M:%S")
|
self.timestamp = datetime.strptime(line.split("[")[1].split("]")[
|
||||||
|
0].split("+")[0].strip(), "%d/%b/%Y:%H:%M:%S")
|
||||||
req = line.split('"')[1].split(' ')
|
req = line.split('"')[1].split(' ')
|
||||||
self.method = req[0]
|
self.method = req[0]
|
||||||
self.path = req[1]
|
self.path = req[1]
|
||||||
self.http = req[2]
|
self.http = req[2]
|
||||||
self.status = int(line.split('"')[2].split(' ')[1])
|
self.status = int(line.split('"')[2].split(' ')[1])
|
||||||
|
|
||||||
|
|
||||||
print("Starting log server")
|
print("Starting log server")
|
||||||
db_conn = pymysql.connect(host=getenv("db_host"),user=getenv("db_user"),password=getenv("db_pass"),db=getenv("db_db"),autocommit=True)
|
db_conn = pymysql.connect(host=getenv("db_host"), user=getenv(
|
||||||
|
"db_user"), password=getenv("db_pass"), db=getenv("db_db"), autocommit=True)
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
s.bind((getenv("tcp_ip"), int(getenv("tcp_port"))))
|
s.bind((getenv("tcp_ip"), int(getenv("tcp_port"))))
|
||||||
s.listen(True)
|
s.listen(True)
|
||||||
@ -40,9 +44,15 @@ while True:
|
|||||||
break
|
break
|
||||||
data = data.decode("utf-8").split("]:")[1].strip()
|
data = data.decode("utf-8").split("]:")[1].strip()
|
||||||
itm = LogItem(data)
|
itm = LogItem(data)
|
||||||
|
while True:
|
||||||
try:
|
try:
|
||||||
with db_conn.cursor() as db:
|
with db_conn.cursor() as db:
|
||||||
db.execute("INSERT INTO `caddy_logs` (`ip`, `timestamp`, `path`, `method`, `http`, `status`) VALUES (%s, %s, %s, %s, %s, %s)",(itm.ip,itm.timestamp,itm.path,itm.method,itm.http,itm.status,))
|
db.execute("INSERT INTO `caddy_logs` (`ip`, `timestamp`, `path`, `method`, `http`, `status`) VALUES (%s, %s, %s, %s, %s, %s)",
|
||||||
|
(itm.ip, itm.timestamp, itm.path, itm.method, itm.http, itm.status,))
|
||||||
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
|
db_conn = pymysql.connect(host=getenv("db_host"), user=getenv(
|
||||||
|
"db_user"), password=getenv("db_pass"), db=getenv("db_db"), autocommit=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
25
anime_scripts/cmpl.sh
Executable file
25
anime_scripts/cmpl.sh
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
name="$1";
|
||||||
|
category="$2";
|
||||||
|
content_path="$3";
|
||||||
|
root_path="$4";
|
||||||
|
save_path="$5";
|
||||||
|
file_number="$6";
|
||||||
|
size="$7";
|
||||||
|
current_tracker="$8";
|
||||||
|
hash="$9";
|
||||||
|
|
||||||
|
export JF_DIR=/Anime/Jellyfin-Anime
|
||||||
|
cd /scripts
|
||||||
|
|
||||||
|
bash notif.sh "$name";
|
||||||
|
|
||||||
|
if [[ "$category" == "Anime" ]]; then
|
||||||
|
echo "Running jellyfin namer";
|
||||||
|
if [[ -d "$root_path" ]]; then
|
||||||
|
bash jellyfin-namer.sh "$root_path";
|
||||||
|
else
|
||||||
|
bash jellyfin-namer.sh "$save_path";
|
||||||
|
fi
|
||||||
|
fi
|
27
anime_scripts/jellyfin-namer.sh
Executable file
27
anime_scripts/jellyfin-namer.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
function get_bottom_dir() {
|
||||||
|
IFS='/';
|
||||||
|
read -ra ADDR <<< "$PWD";
|
||||||
|
echo "${ADDR[-1]}";
|
||||||
|
IFS=' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
function name_clean() {
|
||||||
|
local _out=$(echo "$1" | sed -e 's/\[[^][]*\]//g');
|
||||||
|
_out=$(echo "$_out" | sed -e 's/([^()]*)//g');
|
||||||
|
_out=$(echo "$_out" | sed 's/_/ /g');
|
||||||
|
echo $(echo "$_out" | xargs);
|
||||||
|
}
|
||||||
|
|
||||||
|
cd "$1";
|
||||||
|
|
||||||
|
bottom_dir=$(get_bottom_dir);
|
||||||
|
cleaned_dir=$(name_clean "$bottom_dir");
|
||||||
|
mkdir "$JF_DIR/$cleaned_dir";
|
||||||
|
|
||||||
|
for i in *; do
|
||||||
|
cleaned_name=$(name_clean "$i");
|
||||||
|
ln "$PWD/$i" "$JF_DIR/$cleaned_dir/$cleaned_name" >/dev/null 2>/dev/null;
|
||||||
|
done;
|
||||||
|
|
25
anime_scripts/subtitle-converter.sh
Executable file
25
anime_scripts/subtitle-converter.sh
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
function convert() {
|
||||||
|
res=$(ffprobe -v error -show_entries stream=codec_type,codec_name -of compact "$1" | grep -s "subtitle");
|
||||||
|
if [[ "$res" == *"ass"* ]]; then
|
||||||
|
echo "Converting $1";
|
||||||
|
ffmpeg -n -i "$1" -c:s srt "${i%.$ext}.srt" > /dev/null 2&> /dev/null;
|
||||||
|
else
|
||||||
|
echo "$1 not ASS. Skipping";
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -d "$1" ]]; then
|
||||||
|
cd "$1";
|
||||||
|
shopt -s globstar
|
||||||
|
for i in **/*; do
|
||||||
|
ext="${i##*.}";
|
||||||
|
convert "$i";
|
||||||
|
done
|
||||||
|
elif [[ -f "$1" ]]; then
|
||||||
|
cd $(dirname "$1");
|
||||||
|
name=$(basename "$1");
|
||||||
|
ext="${name##*.}";
|
||||||
|
convert "$name";
|
||||||
|
fi
|
Loading…
x
Reference in New Issue
Block a user