Activity

t

I added a websub switch, a query end point, and made fixes for personal google access tokens.
The websub switch allows the user to set a playlist to not collect new videos. It simply triggers the back end to recheck and sub/unsub as needed.
The query end point uses the youtube search api to search videos based on the matching conditions of a playlist. On the front end, I only finished the date range picker dialog. For some reason it showed as a full screen dialog, but I managed to constraint and crop it to look like a normal dialog.
And I made some fixes to make sure that it checks the access scope of the personal google access token and fall back to the shared token if required, and make sure that it selects the correct token for each playlist.

Attachment
Attachment
0
t

I changed the google access token section in the settings to show scopes instead.
Since revoking one access token revokes all other in the same app, it makes more sense to use incremental authorization and use only one token for all scopes.
To that end, I modified the back end to accept scopes in the oauth2 auth request, then validating that it is between openid, youtube, or youtube.readonly using an AfterValidator. The client id param is still used to indicate the access type and how the data is processed in the token request.
The front end now allows the user to select what scopes they want granted, then go through the same process to add additional scopes. If a scope is removed, it also requests for the current access token to be revoked first as well.

0
t

I added some decorations to the playlist info tile.
I tried a bunch of layout options using Table Row Wrap BoxConstraints, and this is the best I’ve got. I tried to make the cards rearrange themselves and make the tile resize themselves but Wrap doesn’t behave well in a Row and size calculations for constraints will get very complicated.
The current implementation looks horrendous in a larger screen, and out right doesn’t work in a smaller one, but it’ll have to do for now.

Attachment
0
t

The first test was a success!
I had a problem where the Cloud Run doesn’t update with new docker images, but uploading to Google’s Artifact Registry solved it.
And my implementation of the youtube sdk doesn’t work anymore for some reason, so I had to switch back to using google’s official client (I initially switched because of the storage limit of cloudflare workers).
After those fixes, I created a test playlist and added a channel to match. After some time, it successfully added that channel’s new video to my playlist.

Attachment
Attachment
Attachment
0
t

I am finally able to deploy the back end code.
I decided to go with Google’s Cloud Run because of its 2M/month free requests and docker-based deployment. I was also able to deploy it to AWS’s Lambda as well, but it doesn’t support FastAPI directly (had to use Mangum + copy files to zip) and it only has a 1M/month free request limit.
I used google cloud scheduler to trigger the token refresh and websub resub. And I decided to use the OIDC JWT token of a service account to authenticate the refresh request.
I wonder why I didn’t go for this first.

Attachment
0
t

Turns out the limit of cloudflare workers are 3MiB (for free teir).
I tried rewriting my code to not use google’s library, and switching from pyjwt to python-jose. The current size is still around 40MiB.
I’ll be trying other services instead.

Attachment
0
t

I wrote scheduled handlers to refresh google access tokens and refresh websub subscriptions.
They both needs to be refreshed regularly, so I’ll use the cloudflare worker cron triggers to check for it every day.
But before I can deploy it, I encountered another issue; the project is too large. There seems to be a limit to the number and size of files I can upload to cloudflare workers. The source code itself is tiny, but it has many dependencies in the .venv folder. I also tried building it from a git repo, but it doesn’t include the dependencies for some reason.
I’ll be rewriting a lot of sections to pure python and remove as many dependencies as I can.

Attachment
0
t

I completed the websub integration part of the project.
When video matching details are updated, the back end asks youtube’s server to subscribe and unsubscribe to topics for each channel. I simply had to follow the websub specifications about request data and responses. I had some troubles with the query parameters including a dot (“hub.mode”, etc.) but using the query alias solved it.
Every time a video is posted or updated, the websub server will post an xml data about it to my callback uri, I’ll read the video title and id from it, then add the video to playlists if the title match conditions.
Deciding to save matching conditions in a separate table really made querying for unique channel ids easy when I need to update subscriptions. The hours spent determining the best data structure and experimenting with it was worth it.
I also did a ton of research and experimentation with the websub protocol back in SOM, so I only had to rewrite my previous code for FastAPI. It still took some time, but it was a lot easier than working with a protocol I’m not familiar with (looking at you OAuth).
Here’s a video showing the back end automatically adding and rejecting a video according to a regex text, then unsubscribing from the websub topic as the playlist is deleted.

0
t

I initially created this program to redact sensitive information in the demo videos I post for another project, but I thought it might be useful for other people as well so I’ll share it here.
I already finished the PoC which works well enough with some final touches.
The program extracts each frame then uses the PaddleOCR library to recognize words and their bounding boxes.
I’m using a kind of prefix tree to match text across the returned words, and rapidfuzz to fuzzy match words that aren’t caught by the previous match.
There’s still a lot of improvements to be had, and I’ll log my journey here!
Here’s a snippet of this program’s output from my latest devlog demo which has my youtube channel id, handle, and websub callback url redacted.

0
t

I’ve connected all current playlist functions to google api.
That includes: creating, deleting, and updating a playlist. It was relatively simple as google’s api is well documented and there’s a client for python so I only had to create wrapper functions for them. The only things I wish for are type hints for the python client.

0
t

I added a section for google access tokens in the settings.
This allows me to add and share my google access tokens that are used for managing playlists and searching for videos for everyone. It also allows for users to add their own tokens (in case my token reaches its limit or for whatever reason).
At first, I used a special token id “default” to designate the shared token, but I might want to have different tokens for different access scopes or add backup tokens in case of limits, so I added a new column to specify that it is shared instead and query them on the fly instead.

0
t

I added account details and technical details section to the settings page.
The technical details section just lists a lot of info in read-only fields.
The account details section currently only has the account expiration settings. The fields in this section changes the values inside a copy of the user’s data. The data of the copy and the original is compared to see if there are any changes. A FormField widget is also used to validate fields before submitting it to the back end.

0
t

I added some basic dialog with options to edit, delete, and clone a playlist.
I just added an ExpansionTile with some customization, nothing fancy.
What I really struggled with was the request body to update the playlist. It keeps giving me errors that null isn’t string. I learned that there are three main types of request bodies in FastAPI: Form(), Body(), and Body(embed=True). The form type (which I was using) is basically the url-encoded query parameters in the body instead of appended to the url, so it only accepts string keys and values. The body type, however, has the data encoded as json, with the embed option for having a json with the variable name as a key and value as the data.
Flutter (dart) assumes application/x-www-form-urlencoded for body of type Map, text/plain for type String (I think). So I basically had to jsonEncode the body and set the Content-Type header to application/json manually.

0
t

I added a button to switch how to group the matching conditions.
When the value of the drop down changes, the field groups get parsed to match condition list, the group parent type gets changed, then the match list get grouped back into fields.
Previously, I grouped the match list by its text (channel tag / match text) but that would ignore the matching options (case sensitivity / regex), so I changed it to match by the json string representation instead and that solved the problem.
There was also another bug where the parent item wouldn’t update as its child item gets crated. When the user types in the “new field” (with the add button) it automatically adds it then create another “new field”, but as a field in the children list gets created the parent field wouldn’t be created together.
I fixed this by storing the parent field and sibling fields list object inside the field object itself, then calling the parent field’s creation method (add) when the child’s creation method is called.
There is still a bug where the error message would move down when a field is deleted like the field’s content before the controller fix. I have no idea why that happens, so I’ll fix it when I have time.

0
t

I finished the function to submit matching condition data.
I only had to combine the data of the parent and its children fields into a single list. To do that from the validation function, I decided to save the temporary match field groups inside of the playlist class then call the grouping and ungrouping function as needed.

Attachment
0
t

I’ve rewritten the previous part with a custom class.
This class contains the controller, the field attribute type (channel tag / matching text), and whether if it is new, so everything will be removed and added together.
I experimented with storing the level of fields as a coordinate-like system so I can store the final variables in a flat list and support more than two levels (basically creating my own multi-level list type and operations), so adding and removing fields at the same level, among other things, are very complex.
I went with defining a single parent type and a single member type, then add the member fields inside a children variable of the parent field. This solution only supports two levels, but that’s enough for now.

1

Comments

t
t about 2 months ago

Sorry, duplicate video.

t

I somewhat completed the part to add playlist and its video matching conditions.
The idea is for the back end to wait for new video then match it by channel and a text in the title / description and add it to a playlist.
From the back-end server’s perspective, it is best to store the conditions as rows to easily query them.
From the front-end client’s perspective, the user might want to use multiple matching text per one channel or vice versa, and I wanted the dialog UI to work even if the user enters duplicate data, etc. So I converted the list of conditions to list of condition groups, then use it to generate fields and buttons in table rows to manage the layout. It’ll convert the format back when the dialog closes.
I had some issues with the width of the text field, but restricting the total width of the table and adding the Flexible widget around the field solved it.
The main issue I ran into was that the text inside those fields didn’t update as I deleted condition entries. Using a UniqueKey to force an update every setState worked, but it would also defocus the field once a letter is typed (you have to click again to type more). I ended up creating a TextEditingController for each field, then store them in a list mirroring the structure of the text data (+1 more for each new field), then delete the one in the same location of the text data that I am deleting.
I messed around a lot with alternatives until I settled on these solutions that kinda works, but the current implementation is not very scalable, so I’ll rewrite it again with custom classes and see if I can make it better.

0
t

I completed the part to request google access tokens with other scopes to manage playlists.
Originally, after the user logs in with their google account, they’ll be redirected to a callback on the back end with the authorization code, then redirects them to a token processing page on the front end, which exchanges the code for an access token in the background, then redirects the user back to the home page.
But with different type of requests (front end login / Google API access token) coming from different pages, I wanted to redirect the user back to the original page. Therefore I either have to specify the url of the final page in the initial request or hard code the redirection in the back end using different API endpoints.
I tried both but settled on the dynamic solution by specifying the type of request in the client id, then skipping the callback on the back end and have the google login page redirect directly back to the original front end page which has a function to handle the OAuth2 callback query parameters. The pushState function at the end replaces the window url without reloading the page, so if the user reloads the page the query parameters won’t be used again.

Attachment
0
t

The only thing I wanted to confirm with the user before anything else is the account lifetime.
Because I have a limited database storage and worker execution time for my free Cloudflare account, so I wanted to automatically delete the accounts of people who are just testing and don’t need it.
I settled on showing a dialog to the user if the lifetime value is “null” as it is initially in the database. This ensures that I’ll only ask the user about it once without messing with access scopes.

0
t

When creating an account, I wanted to immediately issue an access token after the user authenticates with their google account at the token endpoint to follow the OAuth2 flow specs.
So I experimented with using a temporary access scope “account:create” with a temporary access token to ensure that the user doesn’t access other resources before confirming their intent to create an account.
But I later realized that aside from that there isn’t much I need to confirm, so it’d make my permissions handling logic a lot easier if I just don’t ask for confirmation and let them access other resources straight away.
There goes my 9 hours.

Attachment
0
t

I finished the authentication part of the project.
I decided to have users authenticate using their google account through the OAuth2 authorization code flow. The frontend (client) request an authorization code from google (auth server), then it exchanges that code for an access token with my backend server (token server).
The access token is used for authenticating the user in subsequent requests. It is a JWT containing the user ID and what it is able to access. It being a JWT allows me to include additional information in the token itself which the user can also read, and the server can verify it with its own secret key. It is also kept in a database in case it needs to be revoked before the expiration time.
Google’s id token obtained by exchanging the authorization code is also a JWT containing the user’s google account information (if the “openid” scope is included in the request). The id token’s subject is unique to each user, so it is used to correlate the user id in the backend.
FastAPI has a dependency injection system that ensures the user is authenticated and query the user’s information before entering the function of the endpoint itself.
To-do: Use state tokens to prevent CSRF. Verify G’s id token jti. Implement JWT asymmetric encryption. Increase overall speed.

0
t

I managed to get the vscode flutter debug web to launch in librewolf by setting the launch option to start a web server and creating a custom flutter device with IP 127.0.0.1
With this, I can now use all the debug features without using MS edge or manually hitting reload.

Attachment
1

Comments

t
t 2 months ago

Breakpoints aren’t working, errors aren’t showing, I give up. I’m installing chrome.

t

It takes a long time for the local worker dev server to reload compared to a fastapi server which was run directly, so I decided to develop using fastapi instead.
The problem is that the local worker server injects secrets and D1 bindings to fastapi requests’ scope, but a server ran directly through fastapi can’t access that.
So I created functions to check if it is running through fastapi and let the program read secrets from the local .dev.vars file and access the D1 database through its REST API instead.
This should also allow me to use breakpoints in vscode, saving me hours of headaches later.

Attachment
0
t

fastapi and other 3rd party packages are now supported in Cloudflare python workers. I’ll be migrating the awkward hacks I made in SOM and continue to develop the project here!

Attachment
0