mirror of
https://github.com/dmunozv04/iSponsorBlockTV.git
synced 2025-12-31 03:48:45 +03:00
Merge pull request #44 from oxixes/channel_whitelist
Added channel whitelists and general fixes
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -155,4 +155,6 @@ cython_debug/
|
|||||||
|
|
||||||
#config folder
|
#config folder
|
||||||
config/
|
config/
|
||||||
config.json
|
config.json
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
13
README.md
13
README.md
@@ -6,22 +6,22 @@ This project is written in asycronous python and should be pretty quick.
|
|||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
Check the [wiki](https://github.com/dmunozv04/iSponsorBlockTV/wiki/Installation)
|
Check the [wiki](https://github.com/dmunozv04/iSponsorBlockTV/wiki/Installation)
|
||||||
Warning: armv7 builds have been deprecated
|
|
||||||
|
Warning: armv7 builds have been deprecated.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
Run iSponsorBLockTV in the same network as the Apple TV.
|
Run iSponsorBLockTV on the same network as the Apple TV.
|
||||||
|
|
||||||
It connect to the Apple TV, watch its activity and skip any sponsor segment using the [SponsorBlock](https://sponsor.ajay.app/) API.
|
It connects to the Apple TV, watches its activity and skips any sponsor segment using the [SponsorBlock](https://sponsor.ajay.app/) API.
|
||||||
|
|
||||||
The last 5 videos' segments are cached to limit the number on queries on SponsorBlock and YouTube.
|
The last 5 videos' segments are cached to limit the number on queries on SponsorBlock and YouTube.
|
||||||
|
|
||||||
|
|
||||||
# Libraries used
|
# Libraries used
|
||||||
- [pyatv](https://github.com/postlund/pyatv) Used to connect to the Apple TV
|
- [pyatv](https://github.com/postlund/pyatv) Used to connect to the Apple TV
|
||||||
- [asyncio] and [aiohttp]
|
- asyncio and [aiohttp](https://github.com/aio-libs/aiohttp)
|
||||||
- [async_lru]
|
- [async-cache](https://github.com/iamsinghrajat/async-cache)
|
||||||
- [json]
|
|
||||||
|
|
||||||
# Projects using this proect
|
# Projects using this proect
|
||||||
- [Home Assistant Addon](https://github.com/bertybuttface/addons/tree/main/isponsorblocktv)
|
- [Home Assistant Addon](https://github.com/bertybuttface/addons/tree/main/isponsorblocktv)
|
||||||
@@ -38,5 +38,6 @@ The last 5 videos' segments are cached to limit the number on queries on Sponsor
|
|||||||
|
|
||||||
- [dmunozv04](https://github.com/dmunozv04) - creator and maintainer
|
- [dmunozv04](https://github.com/dmunozv04) - creator and maintainer
|
||||||
- [HaltCatchFire](https://github.com/HaltCatchFire) - updated dependencies and improved skip logic
|
- [HaltCatchFire](https://github.com/HaltCatchFire) - updated dependencies and improved skip logic
|
||||||
|
- [Oxixes](https://github.com/oxixes) - added support for channel whitelist and minor improvements
|
||||||
# License
|
# License
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
{"identifier": "", "airplay_credentials": ""}
|
{"identifier": "", "airplay_credentials": ""}
|
||||||
],
|
],
|
||||||
"apikey":"",
|
"apikey":"",
|
||||||
"skip_categories": ["sponsor"]
|
"skip_categories": ["sponsor"],
|
||||||
|
"channel_whitelist": []
|
||||||
}
|
}
|
||||||
@@ -21,13 +21,45 @@ async def get_vid_id(title, artist, api_key, web_session):
|
|||||||
url = constants.Youtube_api + "search"
|
url = constants.Youtube_api + "search"
|
||||||
async with web_session.get(url, params=params) as resp:
|
async with web_session.get(url, params=params) as resp:
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
|
|
||||||
|
if "error" in data:
|
||||||
|
return
|
||||||
|
|
||||||
for i in data["items"]:
|
for i in data["items"]:
|
||||||
|
if (i["id"]["kind"] != "youtube#video"):
|
||||||
|
continue
|
||||||
title_api = html.unescape(i["snippet"]["title"])
|
title_api = html.unescape(i["snippet"]["title"])
|
||||||
artist_api = html.unescape(i["snippet"]["channelTitle"])
|
artist_api = html.unescape(i["snippet"]["channelTitle"])
|
||||||
if title_api == title and artist_api == artist:
|
if title_api == title and artist_api == artist:
|
||||||
return i["id"]["videoId"]
|
return (i["id"]["videoId"], i["snippet"]["channelId"])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@AsyncLRU(maxsize=10)
|
||||||
|
async def search_channels(channel, api_key, web_session):
|
||||||
|
channels = []
|
||||||
|
params = {"q": channel, "key": api_key, "part": "snippet", "type": "channel", "maxResults": "5"}
|
||||||
|
url = constants.Youtube_api + "search"
|
||||||
|
async with web_session.get(url, params=params) as resp:
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
if "error" in data:
|
||||||
|
return channels
|
||||||
|
|
||||||
|
for i in data["items"]:
|
||||||
|
# Get channel subcription number
|
||||||
|
params = {"id": i["snippet"]["channelId"], "key": api_key, "part": "statistics"}
|
||||||
|
url = constants.Youtube_api + "channels"
|
||||||
|
async with web_session.get(url, params=params) as resp:
|
||||||
|
channelData = await resp.json()
|
||||||
|
|
||||||
|
if channelData["items"][0]["statistics"]["hiddenSubscriberCount"]:
|
||||||
|
subCount = "Hidden"
|
||||||
|
else:
|
||||||
|
subCount = channelData["items"][0]["statistics"]["subscriberCount"]
|
||||||
|
|
||||||
|
channels.append((i["snippet"]["channelId"], i["snippet"]["channelTitle"], subCount))
|
||||||
|
|
||||||
|
return channels
|
||||||
|
|
||||||
@listToTuple
|
@listToTuple
|
||||||
@AsyncTTL(time_to_live=300, maxsize=5)
|
@AsyncTTL(time_to_live=300, maxsize=5)
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import json
|
|||||||
import asyncio
|
import asyncio
|
||||||
from pyatv.const import DeviceModel
|
from pyatv.const import DeviceModel
|
||||||
import sys
|
import sys
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
from . import api_helpers
|
||||||
|
|
||||||
def save_config(config, config_file):
|
def save_config(config, config_file):
|
||||||
with open(config_file, "w") as f:
|
with open(config_file, "w") as f:
|
||||||
@@ -74,9 +76,9 @@ def main(config, config_file, debug):
|
|||||||
try:
|
try:
|
||||||
for i in atvs:
|
for i in atvs:
|
||||||
config["atvs"].append(i)
|
config["atvs"].append(i)
|
||||||
print("done adding")
|
print("Done adding")
|
||||||
except:
|
except:
|
||||||
print("rewriting atvs (don't worry if none were saved before)")
|
print("Rewriting atvs (don't worry if none were saved before)")
|
||||||
config["atvs"] = atvs
|
config["atvs"] = atvs
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -84,12 +86,12 @@ def main(config, config_file, debug):
|
|||||||
except:
|
except:
|
||||||
apikey = ""
|
apikey = ""
|
||||||
if apikey != "":
|
if apikey != "":
|
||||||
if input("Apikey already specified. Change it? (y/n) ") == "y":
|
if input("API key already specified. Change it? (y/n) ") == "y":
|
||||||
apikey = input("Enter your API key: ")
|
apikey = input("Enter your API key: ")
|
||||||
config["apikey"] = apikey
|
config["apikey"] = apikey
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
"get youtube apikey here: https://developers.google.com/youtube/registering_an_application"
|
"Get youtube apikey here: https://developers.google.com/youtube/registering_an_application"
|
||||||
)
|
)
|
||||||
apikey = input("Enter your API key: ")
|
apikey = input("Enter your API key: ")
|
||||||
config["apikey"] = apikey
|
config["apikey"] = apikey
|
||||||
@@ -108,10 +110,58 @@ def main(config, config_file, debug):
|
|||||||
skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings
|
skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings
|
||||||
else:
|
else:
|
||||||
categories = input(
|
categories = input(
|
||||||
"Enter skip categories (space sepparated) Options: [sponsor, selfpromo, exclusive_access, interaction, poi_highlight, intro, outro, preview, filler, music_offtopic:\n"
|
"Enter skip categories (space or comma sepparated) Options: [sponsor, selfpromo, exclusive_access, interaction, poi_highlight, intro, outro, preview, filler, music_offtopic:\n"
|
||||||
)
|
)
|
||||||
skip_categories = categories.split(" ")
|
skip_categories = categories.replace(",", " ").split(" ")
|
||||||
|
skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings
|
||||||
config["skip_categories"] = skip_categories
|
config["skip_categories"] = skip_categories
|
||||||
|
|
||||||
print("config finished")
|
try:
|
||||||
|
channel_whitelist = config["channel_whitelist"]
|
||||||
|
except:
|
||||||
|
channel_whitelist = []
|
||||||
|
|
||||||
|
if input("Do you want to whitelist any channels from being ad-blocked? (y/n) ") == "y":
|
||||||
|
web_session = aiohttp.ClientSession()
|
||||||
|
while True:
|
||||||
|
channel_info = {}
|
||||||
|
channel = input("Enter a channel name or \"/exit\" to exit: ")
|
||||||
|
if channel == "/exit":
|
||||||
|
break
|
||||||
|
|
||||||
|
task = loop.create_task(api_helpers.search_channels(channel, apikey, web_session))
|
||||||
|
loop.run_until_complete(task)
|
||||||
|
results = task.result()
|
||||||
|
if len(results) == 0:
|
||||||
|
print("No channels found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for i in range(len(results)):
|
||||||
|
print(f"{i}: {results[i][1]} - Subs: {results[i][2]}")
|
||||||
|
print("5: Enter a custom channel ID")
|
||||||
|
print("6: Go back")
|
||||||
|
|
||||||
|
choice = -1
|
||||||
|
choice = input("Select one option of the above [0-6]: ")
|
||||||
|
while choice not in [str(x) for x in range(7)]:
|
||||||
|
print("Invalid choice")
|
||||||
|
choice = input("Select one option of the above [0-6]: ")
|
||||||
|
|
||||||
|
if choice == "5":
|
||||||
|
channel_info["id"] = input("Enter a channel ID: ")
|
||||||
|
channel_info["name"] = input("Enter the channel name: ")
|
||||||
|
channel_whitelist.append(channel_info)
|
||||||
|
continue
|
||||||
|
elif choice == "6":
|
||||||
|
continue
|
||||||
|
|
||||||
|
channel_info["id"] = results[int(choice)][0]
|
||||||
|
channel_info["name"] = results[int(choice)][1]
|
||||||
|
channel_whitelist.append(channel_info)
|
||||||
|
# Close web session asynchronously
|
||||||
|
loop.run_until_complete(web_session.close())
|
||||||
|
|
||||||
|
config["channel_whitelist"] = channel_whitelist
|
||||||
|
|
||||||
|
print("Config finished")
|
||||||
save_config(config, config_file)
|
save_config(config, config_file)
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ def app_start():
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
try: # Check if config file has the correct structure
|
try: # Check if config file has the correct structure
|
||||||
config["atvs"], config["apikey"], config["skip_categories"]
|
config["atvs"], config["apikey"], config["skip_categories"], config["channel_whitelist"]
|
||||||
except: # If not, ask to setup the program
|
except: # If not, ask to setup the program
|
||||||
print("invalid config file, please run with --setup")
|
print("invalid config file, please run with --setup")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
main.main(
|
main.main(
|
||||||
config["atvs"], config["apikey"], config["skip_categories"], args.debug
|
config["atvs"], config["apikey"], config["skip_categories"], config["channel_whitelist"], args.debug
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ class MyPushListener(pyatv.interface.PushListener):
|
|||||||
|
|
||||||
web_session = None
|
web_session = None
|
||||||
categories = ["sponsor"]
|
categories = ["sponsor"]
|
||||||
|
whitelist = []
|
||||||
|
|
||||||
def __init__(self, apikey, atv, categories, web_session):
|
def __init__(self, apikey, atv, categories, whitelist, web_session):
|
||||||
self.apikey = apikey
|
self.apikey = apikey
|
||||||
self.rc = atv.remote_control
|
self.rc = atv.remote_control
|
||||||
self.web_session = web_session
|
self.web_session = web_session
|
||||||
self.categories = categories
|
self.categories = categories
|
||||||
|
self.whitelist = whitelist
|
||||||
self.atv = atv
|
self.atv = atv
|
||||||
|
|
||||||
def playstatus_update(self, updater, playstatus):
|
def playstatus_update(self, updater, playstatus):
|
||||||
@@ -37,6 +39,7 @@ class MyPushListener(pyatv.interface.PushListener):
|
|||||||
self.categories,
|
self.categories,
|
||||||
self.atv,
|
self.atv,
|
||||||
time_start,
|
time_start,
|
||||||
|
self.whitelist
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,7 +49,7 @@ class MyPushListener(pyatv.interface.PushListener):
|
|||||||
|
|
||||||
|
|
||||||
async def process_playstatus(
|
async def process_playstatus(
|
||||||
playstatus, apikey, rc, web_session, categories, atv, time_start
|
playstatus, apikey, rc, web_session, categories, atv, time_start, whitelist
|
||||||
):
|
):
|
||||||
logging.debug("App playing is:" + str(atv.metadata.app.identifier))
|
logging.debug("App playing is:" + str(atv.metadata.app.identifier))
|
||||||
if (
|
if (
|
||||||
@@ -57,8 +60,12 @@ async def process_playstatus(
|
|||||||
playstatus.title, playstatus.artist, apikey, web_session
|
playstatus.title, playstatus.artist, apikey, web_session
|
||||||
)
|
)
|
||||||
if vid_id:
|
if vid_id:
|
||||||
print(vid_id)
|
print(f"ID: {vid_id[0]}, Channel ID: {vid_id[1]}")
|
||||||
segments = await api_helpers.get_segments(vid_id, web_session, categories)
|
for i in whitelist:
|
||||||
|
if vid_id[1] == i["id"]:
|
||||||
|
print("Channel whitelisted, skipping.")
|
||||||
|
return
|
||||||
|
segments = await api_helpers.get_segments(vid_id[0], web_session, categories)
|
||||||
print(segments)
|
print(segments)
|
||||||
await time_to_segment(
|
await time_to_segment(
|
||||||
segments, playstatus.position, rc, time_start, web_session
|
segments, playstatus.position, rc, time_start, web_session
|
||||||
@@ -104,12 +111,12 @@ async def connect_atv(loop, identifier, airplay_credentials):
|
|||||||
return await pyatv.connect(config, loop)
|
return await pyatv.connect(config, loop)
|
||||||
|
|
||||||
|
|
||||||
async def loop_atv(event_loop, atv_config, apikey, categories, web_session):
|
async def loop_atv(event_loop, atv_config, apikey, categories, whitelist, web_session):
|
||||||
identifier = atv_config["identifier"]
|
identifier = atv_config["identifier"]
|
||||||
airplay_credentials = atv_config["airplay_credentials"]
|
airplay_credentials = atv_config["airplay_credentials"]
|
||||||
atv = await connect_atv(event_loop, identifier, airplay_credentials)
|
atv = await connect_atv(event_loop, identifier, airplay_credentials)
|
||||||
if atv:
|
if atv:
|
||||||
listener = MyPushListener(apikey, atv, categories, web_session)
|
listener = MyPushListener(apikey, atv, categories, whitelist, web_session)
|
||||||
|
|
||||||
atv.push_updater.listener = listener
|
atv.push_updater.listener = listener
|
||||||
atv.push_updater.start()
|
atv.push_updater.start()
|
||||||
@@ -123,19 +130,19 @@ async def loop_atv(event_loop, atv_config, apikey, categories, web_session):
|
|||||||
# reconnect to apple tv
|
# reconnect to apple tv
|
||||||
atv = await connect_atv(event_loop, identifier, airplay_credentials)
|
atv = await connect_atv(event_loop, identifier, airplay_credentials)
|
||||||
if atv:
|
if atv:
|
||||||
listener = MyPushListener(apikey, atv, categories, web_session)
|
listener = MyPushListener(apikey, atv, categories, whitelist, web_session)
|
||||||
|
|
||||||
atv.push_updater.listener = listener
|
atv.push_updater.listener = listener
|
||||||
atv.push_updater.start()
|
atv.push_updater.start()
|
||||||
print("Push updater started")
|
print("Push updater started")
|
||||||
|
|
||||||
|
|
||||||
def main(atv_configs, apikey, categories, debug):
|
def main(atv_configs, apikey, categories, whitelist, debug):
|
||||||
loop = asyncio.get_event_loop_policy().get_event_loop()
|
loop = asyncio.get_event_loop_policy().get_event_loop()
|
||||||
if debug:
|
if debug:
|
||||||
loop.set_debug(True)
|
loop.set_debug(True)
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
web_session = aiohttp.ClientSession()
|
web_session = aiohttp.ClientSession()
|
||||||
for i in atv_configs:
|
for i in atv_configs:
|
||||||
loop.create_task(loop_atv(loop, i, apikey, categories, web_session))
|
loop.create_task(loop_atv(loop, i, apikey, categories, whitelist, web_session))
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
|
|||||||
Reference in New Issue
Block a user