-
Notifications
You must be signed in to change notification settings - Fork 155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Cast file with local or remote subtitles #93
feat: Cast file with local or remote subtitles #93
Conversation
Daaamn, this is amazing, thank you! @theychx, do you want to review this with me? |
Yep. |
catt/cli.py
Outdated
@@ -103,28 +105,98 @@ def write_config(settings): | |||
raise CattCliError("No device specified.") | |||
|
|||
|
|||
def hunt_subtitle(video): | |||
dot_pos = video.rfind(".") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to use Pathlib here, and also to write a more robust algorithm. My subtitle names aren't always videofile.srt
, sometimes they are videofile.en.srt
and videofile.el.srt
, so it would be good to do some globbing and fetch the first file alphabetically, if nothing is selected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works really well. I've left a few notes.
catt/cli.py
Outdated
@cli.command(short_help="Send a video to a Chromecast for playing.") | ||
@click.argument("video_url", callback=process_url) | ||
@click.argument("subtitle", required=False, default=None, callback=process_subtitle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default=None
is not necessary, as the value becomes None
if the argument is not supplied.
catt/cli.py
Outdated
controller = "default" if force_default else None | ||
cst, stream = setup_cast(settings["device"], video_url=video_url, | ||
prep="app", controller=controller) | ||
|
||
if stream.is_local_file: | ||
click.echo("Casting local file %s..." % video_url) | ||
click.echo("Playing %s on \"%s\"..." % (stream.video_title, cst.cc_name)) | ||
subtitle_url = load_subtitle_if_exists(subtitle, video_url, stream) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer if you supplied port
and local_ip
to load_subtitle_if_exists
instead of the entire stream
object.
catt/cli.py
Outdated
# it's an URL | ||
return subtitle | ||
|
||
if len(subtitle) > 4 and subtitle.lower()[-4:] == ".srt": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if subtitle.lower().endswith(".srt")
would be more concise.
catt/cli.py
Outdated
|
||
def load_subtitle_if_exists(subtitle, video, stream): | ||
if subtitle is None: | ||
subtitle = hunt_subtitle(video) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
subtitle = subtitle or hunt_subtitle(video)
would be more concise.
catt/cli.py
Outdated
|
||
if "://" in subtitle: | ||
# it's an URL | ||
return subtitle |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're not converting remote srt subtitles.
Hi! Thanks for the nice review. I agree 100% with all the mentioned items. Congratulations once more for |
Sorry, I was not clear in my last message. I implemented everything you mentioned and also added an extra parameter --dont-load-subtitles-automatically, which could sometimes (readonly) be necessary |
catt/cli.py
Outdated
continue | ||
if entry_path.stem.lower().startswith(video_path_stem_lower) and \ | ||
entry_path.suffix.lower() in [".vtt", ".srt"]: | ||
return entry |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not working, as you're not returning the absolute path. You could do this:
for entry_path in video_path.parent.iterdir()
(and thus, no longer need os
)
And then let the function return str(entry_path.resolve())
catt/cli.py
Outdated
controller = "default" if force_default else None | ||
cst, stream = setup_cast(settings["device"], video_url=video_url, | ||
prep="app", controller=controller) | ||
|
||
if stream.is_local_file: | ||
click.echo("Casting local file %s..." % video_url) | ||
click.echo("Playing %s on \"%s\"..." % (stream.video_title, cst.cc_name)) | ||
subtitle_url = load_subtitle_if_exists(subtitle, dont_load_subtitles_automatically, video_url, stream.local_ip, stream.port + 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer if the flag-handling logic was here, instead of the flag being passed on to the function. Like:
if subtitle is None and dont_load_subtitles_automatically:
subtitle_url = None
else:
subtitle_url = load_subtitle_if_exists(subtitle, video_url, stream.local_ip, stream.port + 1)
Sorry for the delay. |
By the way, @theychx I don't know what's wrong with |
I've had issues with them being flaky as well. Usually just rerunning the test works. |
0bba65d
to
42dd55d
Compare
@skorokithakis I rebased to make sure travis would run again and it failed again. My other PR also failed again, for the same reason... any ideas ? |
@marcosdiez The travis fails do not seem to be caused by anything in your code. I have no idea whats going on. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marcosdiez This is really cool! Thanks!
I have fixed the Travis failure. Let me see if I can update this branch with the fix. |
catt/cli.py
Outdated
video_path = Path(video) | ||
video_path_stem_lower = video_path.stem.lower() | ||
for entry in video_path.parent.iterdir(): | ||
entry_path = Path(entry) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this necessary? Aren't the returned objects paths already?
catt/cli.py
Outdated
|
||
|
||
def convert_srt_to_webvtt_helper(content): | ||
content = re.sub(r'([\d]+)\,([\d]+)', r'\1.\2', content) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems a bit too general, it will catch things like internationalized numbers in the subtitles and mistakenly convert them. Can we make it more specific?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works, if there aren't more SRT formats:
# Find all lines that contain `-->` (timing lines) and replace commas in them with periods.
re.sub(r"^(.*? \-\-\> .*?)$", lambda m: m.group(1).replace(",", "."), content, flags=re.MULTILINE)
catt/cli.py
Outdated
|
||
|
||
def convert_srt_to_webvtt(filename): | ||
# print("Converting {} to WebVTT".format(filename)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment here should be removed.
catt/cli.py
Outdated
|
||
def convert_srt_to_webvtt(filename): | ||
# print("Converting {} to WebVTT".format(filename)) | ||
for possible_encoding in ['utf-8', 'iso-8859-15']: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use chardet here later on.
catt/cli.py
Outdated
|
||
|
||
def load_subtitle_if_exists(subtitle, video, local_ip, port): | ||
subtitle = subtitle or hunt_subtitle(video) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry to change this again, but the ternary if
is more readable here:
subtitle = subtitle if subtitle else hunt_subtitle(video)
return subtitle | ||
|
||
if subtitle.lower().endswith(".srt"): | ||
subtitle = convert_srt_to_webvtt(subtitle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this give an error unless the extension is .srt
or .vtt
instead? We wouldn't want to continue with any arbitrary file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to https://developers.google.com/cast/docs/media , Chromecast supports:
TTML - Timed Text Markup Language
(example, https://gist.github.com/anotherhale/676a72edc84ca3a37c0c )
WebVTT - Web Video Text Tracks
(example: https://github.com/brenopolanski/html5-video-webvtt-example/blob/master/MIB2-subtitles-pt-BR.vtt
Line 21 Data Services
example: https://en.wikipedia.org/wiki/EIA-608
I confess I never heard about any of these 3 and I bet nobody will ever use cast with the latter. And XML is a bitch extension, so I think we are fine doing what the user tells us to instead of complaining.
What do you think ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that your code is not failing if the specified subtitle file has neither an ".srt" or ".vtt" extension.
(you are right that we should not concern ourselves with the other two obscure formats)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point is there may be somebody who would like to use one of the obscure formats and if we arbitrarily forbid it because the extensions we are restricting for restriction's sake.
In the end of the day there is no downside in sending the wrong file to chromecast as an extension, so I think we should not restrict it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, we don't check the extension for the main files we cast, so we should not bother with the subtitles either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, okay, this makes sense to me, thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost done, I just have a few comments and then we're good to merge!
catt/cli.py
Outdated
@cli.command(short_help="Send a video to a Chromecast for playing.") | ||
@click.argument("video_url", callback=process_url) | ||
@click.argument("subtitle", required=False, callback=process_subtitle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would be better served as an option, rather than as an argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, but that would also change the applicability of --dont-load-subtitles-automatically
. Because if this was an option, you might as well just say that --subtitle
without any argument means "try to automatically load subtitle".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly. And --no-subtitle
would mean "don't load, even automatically".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe NOT specifying --subtitle
would suffice 😃 (I'll butt out of your review now. Sorry).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that there are three states, file/auto/none. So we need --subtitle
/nothing/--no-subtitle
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not specifying --subtitle
means none.
Specifying --subtitle
without an argument means auto.
Specifying --subtitle <subtitle>
means file.
What am I missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer if auto loading subtitles was the default option. That's why I wrote this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We both agree with that, we're just discussing the interface, basically. It's a bit tricky. Maybe --subs/--no-subs
and it automatically discovers if you specify neither?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So:
Nothing specified means "auto".
Specifying --subs <subtitle-file>
means "file".
Specifying --no-subs
means "none".
Having given this a bit of thought, I can see that this is probably the behaviour most users would expect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good, we all agreed on the same thing. I just got somebody visiting me, so I'll continue later.
catt/cli.py
Outdated
@click.option("-f", "--force-default", is_flag=True, | ||
help="Force use of the default Chromecast app (use if a custom app doesn't work).") | ||
@click.option("-r", "--random-play", is_flag=True, | ||
help="Play random item from playlist, if applicable.") | ||
@click.option("--dont-load-subtitles-automatically", is_flag=True, default=False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be renamed to --no-subs
? It's kind of long right now.
catt/cli.py
Outdated
@click.pass_obj | ||
def cast(settings, video_url, force_default, random_play): | ||
def cast(settings, video_url, subtitle, force_default, random_play, dont_load_subtitles_automatically): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having subtitle
be an option would work better here as well, as I don't like changing function signatures that drastically. However, we don't have default arguments for this, so it'd break anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I misread "subtitle without any argument". I think loading the associated sub should be the default behaviour, without needing an argument...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I.e. without needing to specify --subtitle
, I mean.
Thanks again for this PR, @marcosdiez, this is extremely useful. I'll release 0.7.0 as soon as we merge. |
catt/cli.py
Outdated
controller = "default" if force_default else None | ||
cst, stream = setup_cast(settings["device"], video_url=video_url, | ||
prep="app", controller=controller) | ||
|
||
if stream.is_local_file: | ||
click.echo("Casting local file %s..." % video_url) | ||
click.echo("Playing %s on \"%s\"..." % (stream.video_title, cst.cc_name)) | ||
if subtitle is None and dont_load_subtitles_automatically: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason we can't cast remote files with local subtitles as well? It's going to be a bit more tricky, but if it's easy to do, why not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would not work with save/restore, as we would face the same problem as with save/restore for local files. And if remote files + local subtitles was implemented, users would probably expect this to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, alright.
Please wait a few days before releasing. |
I added |
@marcosdiez What is the 7653225 commit doing in this PR? |
@@ -9,4 +9,5 @@ install: "pip install -r requirements_dev.txt" | |||
# command to run tests, e.g. python setup.py test | |||
script: | |||
- python setup.py test | |||
- flake8 --version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably unnecessary, the version is printed when installing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can take it out, but for me it was very useful.
I was doing some other development here and flake was telling me other stuff. If you check it out I have PR called flake8 is happy again
and another one called Revert "flake8 is happy again"
, because different flake versions give me different output, so that's the easy way to know what sublibraries flake8
has.
I applied the same trick here https://github.com/jstasiak/python-zeroconf/pulls and that's why I am the only one with a passing build. I would keep it. It does not affect the end user and make the devs happier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's true, I just generally dislike putting too much stuff in PRs. This is fine though, thanks.
class FileHandler(BaseHTTPRequestHandler): | ||
|
||
def format_size(self, size): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very useful debugging information.
Now we get this kind of logs:
10.1.1.3 - - [07/May/2018 11:53:56] "GET /?loaded_from_catt HTTP/1.1" 200 - video/mp4 - 39.80 MB
10.1.1.3 - - [07/May/2018 11:53:56] "GET //tmp/tmpmnfqm1im.vtt HTTP/1.1" 200 - text/vtt;charset=utf-8 - 125.97 KB
I backported from my other PR.
I like knowing how big the file and the mime is useful for the other one anyway.
Alright, this looks good to me! @theychx, any feedback/objections? |
Looks cool! (and passes my testing scheme) |
Great, approved! Thank you, @marcosdiez! |
NIIIIIIIIIIIIIIIIIICE ! |
fixes #46