Skip to content
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

Netflix plugin #23

Closed
wants to merge 5 commits into from
Closed

Netflix plugin #23

wants to merge 5 commits into from

Conversation

Kelerchian
Copy link

No description provided.

@Kelerchian
Copy link
Author

Kelerchian commented Oct 6, 2020

Context related code in this commit is disabled due to this weird context-related behavior in Lipsurf #24
It is possibly a bug.

@Kelerchian Kelerchian marked this pull request as draft October 6, 2020 16:23
@Kelerchian Kelerchian marked this pull request as ready for review October 8, 2020 14:34
Copy link
Contributor

@mikob mikob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job! Some questions and requests in the comments. Also, the plugin in dist/ should be built with build:prod

return handleChangeAudioAnswer(command);
}
}
} catch {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to not catch in that case if anything to let the error propagate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice you catch errors in many places below. Sometimes it makes more sense to let the error propagate up. Right now, the page will catch pageFn errors and log them so we know which commands are failing where. But also we will likely give some sort of user feedback when a command fails (eg. make the live transcript turn red) in a subsequent version.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually intend to catch only the JSON.parse part. Not the other.
I think I'll make that clearer. The usage will be something like this.

consumeCommand(maybeCommand, proofKey, (command) => {
  ...
})


// Context

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment can be removed now

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this

src/Netflix/Netflix.ts Outdated Show resolved Hide resolved
src/Netflix/Netflix.ts Show resolved Hide resolved
contexts: {
[NetflixPluginContextEnum.watch]: {
commands: [
"Watch::Pause",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't match the format of existing commands. You should take a look at the other plugins so things are consistent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I'll rename them

const tracks = sub.texts;
const [index] = await PluginBase.util.fuzzyHighScore(
sub.query,
tracks.map(track => track.displayName).map(stripNonAlphaNumericAndTrim),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, fuzzyHighScore should do both invalid char stripping and trimming internally. Did you have problems without this stripNonAlphaNumericAndTrim? Can you give me examples?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually did this on a whim. I did not run it without that stripNonAlphaNumericAndTrim. Should I remove it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. Also, the version of this that platform uses is language-aware so it's "smarter"

src/Netflix/Netflix.ts Show resolved Hide resolved
// Messaging

const LIPSURF_BOOT_SCRIPT_ID = "lipsurf-netflix-script";
const TO_PAGE_PROOF_KEY = String(Math.floor(Math.random() * 1000));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool! Any reason to not just use a static string prepended with PluginBase.util.getNoCollisionUniqueAttr() ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.) I didn't realize it is there when I skimmed the docs and 2.) I didn't expect that function to exists in the platform utils (probably the latter influence the former).
I'll change it to that


{
name: "Watch::Title",
match: ["watch *"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, but ideally we would only match valid shows. So you could cache the shows and then use a matchFn with the util's fuzzy matching algorithm to pick the right one. It might be performance prohibitive depending on how big the show db is.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am optimistic about devices that can run Chrome, can withhold fuzzy matching for tens of thousands less-than-100-bytes utf8 string. And shows stored on Netflix's cache is probably can reach just about hundreds.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also found out that LipSurf reads Aria-Label so I kind of utilize that rather than the fuzzy matching, but I guess I can pull out the data from Netflix cache back to the extension context ala "ChangeText" and "ChangeAudio".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up combining fuzzyHighScore with 0.8 minimum confidence and falling back to search if no matching videos. The overall UX feels better that way. I found a weird behavior with fuzzyHighScore though.

When I manually map an array of videos to fuzzyHighScore with the video as the only source and then sort the result in descending order, I got a different result to when I call the fuzzyHighScore with all videos put to sources.

const [index,title] = await fuzzyHighScores(query, videoTitles, 0, true);

returns a different result (highest score) than the code below

cosnt {index, title} = (await Promise.all(
  videoTitles.map(async title => ({
    score: await fuzzyHighScores(query, [title], 0, true)
  }))
))
  // Maps array index as the index, rather than the fuzzyIndex which is always 0
  .map(([_indexFromFuzzy, score], arrayIndex) => ({
    score,
    index: arrayIndex
  }))
  // Sort descending
  .sort(({scoreA},{scoreB}) => scoreB - scoreA)[0]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"I ended up combining fuzzyHighScore with 0.8 minimum confidence and falling back to search if no matching videos. The overall UX feels better that way." that sounds good.
"I got a different result" -- that doesn't seem right. Can you give me the inputs you got those results with?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you go.
There's also the code and result.
https://gist.github.com/Kelerchian/df50a53a9ef08cece06fdc326a9c1cfe

src/Netflix/Netflix.ts Show resolved Hide resolved
@Kelerchian Kelerchian requested a review from mikob October 9, 2020 16:04
Copy link
Contributor

@mikob mikob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First PR always gets a very thorough review!

}
};

const consumeMessageStringAsCommand = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the JSON parsing fails, then message is null and the consumeMessageStringAsCommand cb never gets called. That wouldn't be straightforward to track down. On the other hand, if you don't catch the JSON.parse error, it propogates up, gets caught by bugsnag and/or the developer looking at the console will see it right away. Hence I think it would be better to just call JSON.parse without any try-catch.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a counter-argument for this.

In this plugin's context, a valid inter-context command message is defined as:

  • a JSON
  • has a proofKey field with a certain value determined at the start of the javascript runtime
  • has a payload field with type Command

consumeMessageStringAsCommand is used to consume string as a command message and is used in the "message" event on windows. There's a chance Netflix have inter-frame message-passing mechanisms somewhere in their app and those message, while probably are also JSON, can be in other format too because JSON is not in the spec.

So my argument is if a string coming from window.addEventListener("message", ...) is not a valid Message then the plugin should not be concerned with it. Because the plugin should not be concerned with it, any error that comes from it should not be handled, because it is irrelevant. Therefore, in this case, it is better for consumeMessageStringAsCommand to silently swallow any errors that come from invalid incoming data.

If it's the debug-friendliness you are concerned with, this scenario fits more into unit-tests rather than runtime-debugging, staying close to "type error should be resolved on compile-time" principle.

That's my two cents, but the final decision will stay in your hands.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I understand what you mean better now. This is good and fine. You can leave it as-is.

@@ -333,7 +373,7 @@ export default <IPluginBase & IPlugin>{
normal: false
},
{
name: "ChangeText",
name: "Change Text",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't catch this last time, but "Change Text" could be clearer. Perhaps "Change Subtitle Language" Also, the match phrase could have an "[/language]" after "[text/subtitle]"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing "Change Text" to "Change Subtitle Language". FYI Text is Netflix's formal definition of subtitle.

Skipping the "[/language]". The command "Change Audio" also deals with language. That makes the word "language" ambiguous to the user.

@@ -333,7 +373,7 @@ export default <IPluginBase & IPlugin>{
normal: false
},
{
name: "ChangeText",
name: "Change Text",
match: ["[change/switch] [text/subtitle] to *", "[text/subtitle] to *"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can just be "[/change /switch ][text/subtitle] to *"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice


{
name: "Watch::Title",
match: ["watch *"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"I ended up combining fuzzyHighScore with 0.8 minimum confidence and falling back to search if no matching videos. The overall UX feels better that way." that sounds good.
"I got a different result" -- that doesn't seem right. Can you give me the inputs you got those results with?

src/Netflix/Netflix.ts Show resolved Hide resolved
src/Netflix/Netflix.ts Show resolved Hide resolved
src/Netflix/Netflix.ts Show resolved Hide resolved
const results = (
await Promise.all(
filteredVideos.map(video =>
PluginBase.util.fuzzyHighScore(strippedQuery, [video.title], 0, true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you shouldn't need to strip the query, that string gets preprocessed the same way the sources do.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok


// Filter out videos which title are empty string
const filteredVideos = videos.filter(
video => !!stripNonAlphaNumericAndTrim(video.title)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't need to do the stripping here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Keeping the trim because videos sometimes has empty string as title

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need trim either.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as below
Notice that this is not .map. This does not change the title by trimming, this removes video data which title is empty.
This relates more to data cleanup rather than data processing itself (the fuzzyHighScore).

I initially wrote this because of the fuzzyHighScore returns different result when mapped vs unmapped.

I think this is unecessary with it written mapped (result sorted manually)

const tracks = sub.texts;
const [index] = await PluginBase.util.fuzzyHighScore(
sub.query,
tracks.map(track => track.displayName).map(stripNonAlphaNumericAndTrim),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. Also, the version of this that platform uses is language-aware so it's "smarter"

Copy link
Contributor

@mikob mikob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't reproduce the context dupes issue. I put this in init. Can you give me a minimal sample to reproduce the issue?

        // prints: before Normal
        console.log('before ' + PluginBase.util.getContext());
        PluginBase.util.addContext('new context');
        PluginBase.util.addContext('new context');
        // prints: after Normal, new context
        console.log('after ' + PluginBase.util.getContext());

@@ -360,7 +357,7 @@ export default <IPluginBase & IPlugin>{
},
{
name: "Change Audio",
match: ["[change/switch] audio to *", "audio to *"],
match: ["[/change/switch] audio to *"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will create a match string with a leading space. You should include the space like this: "[/change /switch ]audio to *"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(other match strings below also need to be adjusted for this leading space)

Copy link
Author

@Kelerchian Kelerchian Oct 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.
Saying "audio to *" works though with this. Will change to that.

src/Netflix/Netflix.ts Show resolved Hide resolved
@mikob
Copy link
Contributor

mikob commented Oct 13, 2020

Sorry, haven't replied to some of your previous comments yet but closed the review. Will reply to them soon.

@Kelerchian
Copy link
Author

Sorry, haven't replied to some of your previous comments yet but closed the review. Will reply to them soon.

Take it easy.

I couldn't reproduce the context dupes issue. I put this in init. Can you give me a minimal sample to reproduce the issue?

        // prints: before Normal
        console.log('before ' + PluginBase.util.getContext());
        PluginBase.util.addContext('new context');
        PluginBase.util.addContext('new context');
        // prints: after Normal, new context
        console.log('after ' + PluginBase.util.getContext());

I'll give you this later.

@Kelerchian
Copy link
Author

I couldn't reproduce the context dupes issue. I put this in init. Can you give me a minimal sample to reproduce the issue?

        // prints: before Normal
        console.log('before ' + PluginBase.util.getContext());
        PluginBase.util.addContext('new context');
        PluginBase.util.addContext('new context');
        // prints: after Normal, new context
        console.log('after ' + PluginBase.util.getContext());

Sorry, it turns out I made a noob mistake. This is a false alarm.

Copy link
Contributor

@mikob mikob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically ready. Just some minor things left. Thanks for you patience.

switch (true) {
case context === NetflixPluginContextEnum.browse: {
PluginBase.util.removeContext(NetflixPluginContextEnum.watch);
PluginBase.util.addContext(NetflixPluginContextEnum.browse);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you try using enterContext to be more explicit about the context ordering? Also I think it will fix the "search" homophone issue.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will need a quite complex sequence to ensure tag works, though. I'll try using enterContext

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works. I'm not sure it is ideal though.

* TODOs:
* - Remove ContextPatcher stuff when context duplication caused by PluginBase.util.addContext is fixed
* - Remove homophone `"search": "search"` after homophone priority issue is fixed on the Platform-land
* https://discuss.lipsurf.com/t/homophones-must-be-a-lower-priority-than-other-plugins-command/297/3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the issue is that the Normal context is taking priority because it's appearing first in the context order. If you explicitly enter the context with Normal coming after the Netflix-specific contexts, I think this will be fixed. Sorry, docs didn't mention this this so I've updated them: https://docs.lipsurf.com/contexts.html#context-order

}
};

const consumeMessageStringAsCommand = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I understand what you mean better now. This is good and fine. You can leave it as-is.

src/Netflix/Netflix.ts Show resolved Hide resolved
mikob added a commit that referenced this pull request Oct 16, 2020
@mikob
Copy link
Contributor

mikob commented Oct 16, 2020

Merged into monorepo. Nice work!

@mikob mikob closed this Oct 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants