Skip to content

Commit

Permalink
fc2: emit events, add apprise notifier to autofc2
Browse files Browse the repository at this point in the history
Closes #27
  • Loading branch information
hizkifw committed Jul 17, 2023
1 parent eb4d498 commit 08421c6
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 19 deletions.
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Tool to download FC2 live streams
[![PyPI](https://img.shields.io/pypi/v/fc2-live-dl)](https://pypi.org/project/fc2-live-dl/ 'PyPI')
[![PyPI](https://img.shields.io/pypi/v/fc2-live-dl)](https://pypi.org/project/fc2-live-dl/ "PyPI")

## Requirements

Expand All @@ -17,6 +17,8 @@
- Remux recordings to .mp4/.m4a after it's done
- Continuously monitor multiple streams in parallel and automatically start
downloading when any of them goes online
- Get notifications when streams come online via
[Apprise](https://github.com/caronc/apprise)

## Installation

Expand Down Expand Up @@ -152,6 +154,12 @@ Where the `autofc2.json` file looks like this:
"extract_audio": true,
"trust_env_proxy": false
},
"notifications": [
{
"url": "discord://{WebhookID}/{WebhookToken}",
"message": "%(channel_name)s is live!\nhttps://live.fc2.com/%(channel_id)s"
}
],
"channels": {
"91544481": {
"_en_name": "Necoma Karin",
Expand All @@ -175,6 +183,19 @@ accessible in `outtmpl`. This is useful for specifying custom filenames just
like in the example above. In the example I'm using `_en_name`, but you can use
anything as long as it starts with `_`.

For notifications, the URL follows the
[Apprise syntax](https://github.com/caronc/apprise#supported-notifications). For
example, if you want to use Discord webhooks, use the `discord://` like so:

- Original URL: `https://discord.com/api/webhooks/12341234/abcdabcd`
- Turns into: `discord://12341234/abcdabcd`

You can find out more about the different types of notifiers and how to
configure them on
[Apprise's GitHub](https://github.com/caronc/apprise#supported-notifications).

The `message` of the notifications follow the same syntax as `outtmpl`.

**NOTE Windows users**: When specifying a file path (e.g. for cookies) in the
json, double up your backslashes, for example:
`"cookies_file": "C:\\Documents\\cookies.txt"`.
Expand Down
111 changes: 98 additions & 13 deletions fc2_live_dl/FC2LiveDL.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pathlib
import time
from datetime import datetime
from enum import Enum

import aiohttp

Expand All @@ -16,6 +17,25 @@
from .util import Logger, sanitize_filename


class CallbackEvent:
class Type(Enum):
WAITING_FOR_ONLINE = 1
STREAM_ONLINE = 2
WAITING_FOR_TARGET_QUALITY = 3
GOT_HLS_URL = 4
FRAGMENT_PROGRESS = 5
MUXING = 6

def __init__(self, instance, channel_id, type: Type, data=None):
self.instance = instance
self.channel_id = channel_id
self.type = type
self.data = data

def __repr__(self):
return f"CallbackEvent({self.channel_id}, {self.type}, {self.data})"


class FC2LiveDL:
# Constants
STREAM_QUALITY = {
Expand Down Expand Up @@ -47,14 +67,14 @@ class FC2LiveDL:
"keep_intermediates": False,
"extract_audio": False,
"trust_env_proxy": False,
# Debug params
"dump_websocket": False,
}

def __init__(self, params={}):
def __init__(self, params={}, callback=None):
self._logger = Logger("fc2")
self._session = None
self._background_tasks = []
self._callback = callback if callback is not None else lambda e: None

self.params = json.loads(json.dumps(self.DEFAULT_PARAMS))
self.params.update(params)
Expand Down Expand Up @@ -111,9 +131,24 @@ async def download(self, channel_id):
if not is_online:
if not self.params["wait_for_live"]:
raise FC2LiveStream.NotOnlineException()
self._callback(
CallbackEvent(
self,
channel_id,
CallbackEvent.Type.WAITING_FOR_ONLINE,
)
)
await live.wait_for_online(self.params["wait_poll_interval"])

meta = await live.get_meta(refetch=False)
self._callback(
CallbackEvent(
self,
channel_id,
CallbackEvent.Type.STREAM_ONLINE,
meta,
)
)

fname_info = self._prepare_file(meta, "info.json")
fname_thumb = self._prepare_file(meta, "png")
Expand Down Expand Up @@ -170,6 +205,18 @@ async def download(self, channel_id):
self.params["wait_for_quality_timeout"],
),
)
self._callback(
CallbackEvent(
self,
channel_id,
CallbackEvent.Type.WAITING_FOR_TARGET_QUALITY,
{
"requested": self._format_mode(mode),
"available": self._format_mode(got_mode),
"hls_info": hls_info,
},
)
)
await asyncio.sleep(1)

if got_mode != mode:
Expand All @@ -178,14 +225,27 @@ async def download(self, channel_id):
self._format_mode(got_mode),
)

self._callback(
CallbackEvent(
self,
channel_id,
CallbackEvent.Type.GOT_HLS_URL,
{
"requested": self._format_mode(mode),
"available": self._format_mode(got_mode),
"hls_url": hls_url,
},
)
)

self._logger.info("Received HLS info")

coros = []

coros.append(ws.wait_disconnection())

self._logger.info("Writing stream to", fname_stream)
coros.append(self._download_stream(hls_url, fname_stream))
coros.append(self._download_stream(channel_id, hls_url, fname_stream))

if self.params["write_chat"]:
self._logger.info("Writing chat to", fname_chat)
Expand Down Expand Up @@ -228,12 +288,14 @@ async def download(self, channel_id):
and os.path.isfile(fname_stream)
):
self._logger.info("Remuxing stream to", fname_muxed)
await self._remux_stream(fname_stream, fname_muxed)
await self._remux_stream(channel_id, fname_stream, fname_muxed)
self._logger.debug("Finished remuxing stream", fname_muxed)

if self.params["extract_audio"]:
self._logger.info("Extracting audio to", fname_audio)
await self._remux_stream(fname_stream, fname_audio, extra_flags=["-vn"])
await self._remux_stream(
channel_id, fname_stream, fname_audio, extra_flags=["-vn"]
)
self._logger.debug("Finished remuxing stream", fname_muxed)

if not self.params["keep_intermediates"] and os.path.isfile(fname_muxed):
Expand All @@ -246,7 +308,7 @@ async def download(self, channel_id):

self._logger.info("Done")

async def _download_stream(self, hls_url, fname):
async def _download_stream(self, channel_id, hls_url, fname):
def sizeof_fmt(num, suffix="B"):
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
if abs(num) < 1024.0:
Expand All @@ -272,12 +334,23 @@ def sizeof_fmt(num, suffix="B"):
sizeof_fmt(total_size),
inline=True,
)
self._callback(
CallbackEvent(
self,
channel_id,
CallbackEvent.Type.FRAGMENT_PROGRESS,
{
"fragments_downloaded": n_frags,
"total_size": total_size,
},
)
)
except asyncio.CancelledError:
self._logger.debug("_download_stream cancelled")
except Exception as ex:
self._logger.error(ex)

async def _remux_stream(self, ifname, ofname, *, extra_flags=[]):
async def _remux_stream(self, channel_id, ifname, ofname, *, extra_flags=[]):
mux_flags = [
"-y",
"-hide_banner",
Expand All @@ -295,6 +368,7 @@ async def _remux_stream(self, ifname, ofname, *, extra_flags=[]):
]
async with FFMpeg(mux_flags) as mux:
self._logger.info("Remuxing stream", inline=True)
self._callback(CallbackEvent(self, channel_id, CallbackEvent.Type.MUXING))
while await mux.print_status():
pass

Expand Down Expand Up @@ -383,7 +457,8 @@ def get_unique_name(meta, ext):
fpath.parent.mkdir(parents=True, exist_ok=True)
return fname

def _format_outtmpl(self, meta=None, overrides={}):
@classmethod
def get_format_info(cls, *, meta=None, params={}, sanitize=False):
finfo = {
"channel_id": "",
"channel_name": "",
Expand All @@ -393,15 +468,25 @@ def _format_outtmpl(self, meta=None, overrides={}):
"ext": "",
}

sanitizer = sanitize_filename if sanitize else lambda x: x

if meta is not None:
finfo["channel_id"] = sanitize_filename(meta["channel_data"]["channelid"])
finfo["channel_name"] = sanitize_filename(meta["profile_data"]["name"])
finfo["title"] = sanitize_filename(meta["channel_data"]["title"])
finfo["channel_id"] = sanitizer(meta["channel_data"]["channelid"])
finfo["channel_name"] = sanitizer(meta["profile_data"]["name"])
finfo["title"] = sanitizer(meta["channel_data"]["title"])

for key in self.params:
for key in params:
if key.startswith("_"):
finfo[key] = self.params[key]
finfo[key] = params[key]

return finfo

def _format_outtmpl(self, meta=None, overrides={}):
finfo = FC2LiveDL.get_format_info(
meta=meta,
params=self.params,
sanitize=True,
)
finfo.update(overrides)

formatted = self.params["outtmpl"] % finfo
Expand Down
1 change: 0 additions & 1 deletion fc2_live_dl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ async def _main(args):
"keep_intermediates": args.keep_intermediates,
"extract_audio": args.extract_audio,
"trust_env_proxy": args.trust_env_proxy,
# Debug params
"dump_websocket": args.dump_websocket,
}

Expand Down
32 changes: 29 additions & 3 deletions fc2_live_dl/autofc2.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import argparse
import asyncio
import copy
import json

from .FC2LiveDL import FC2LiveDL
import apprise

from .FC2LiveDL import FC2LiveDL, CallbackEvent
from .util import Logger


Expand Down Expand Up @@ -87,9 +88,34 @@ async def config_watcher(self):
Logger.loglevel = Logger.LOGLEVELS[log_level]
self.logger.info(f"Setting log level to {log_level}")

def handle_event(self, event):
try:
if event.type != CallbackEvent.Type.STREAM_ONLINE:
return

config = self.get_config()
finfo = FC2LiveDL.get_format_info(
meta=event.data,
params=event.instance.params,
sanitize=False,
)

if "notifications" not in config:
return

for cfg in config["notifications"]:
notifier = apprise.Apprise()
notifier.add(cfg["url"])
notifier.notify(body=cfg["message"] % finfo)

except Exception as ex:
self.logger.error("Error handling event")
self.logger.error(ex)
return

async def handle_channel(self, channel_id):
params = self.get_channel_params(channel_id)
async with FC2LiveDL(params) as fc2:
async with FC2LiveDL(params, self.handle_event) as fc2:
await fc2.download(channel_id)

async def _main(self):
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
aiohttp>=3.7.4.post0
apprise>=1.4.5
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = fc2-live-dl
version = 2.1.3
version = 2.2.0
author = Hizkia Felix
author_email = [email protected]
description = Download live streams from FC2
Expand All @@ -26,6 +26,7 @@ packages = find:
install_requires =
aiohttp >= 3.8.1
aiodns >= 3.0.0
apprise >= 1.4.5

[options.entry_points]
console_scripts =
Expand Down

0 comments on commit 08421c6

Please sign in to comment.