Integrate MyAnimeList tracking and enhance Live TV experience - #25
Merged
Conversation
Live TV already told the player what it was handing over — `type: 'live'` — and the player never read it. Whether a stream was live was guessed from its duration alone, and plenty of live HLS carries a DVR window and reports a perfectly finite one. Those channels were treated as files: a scrub bar over something unscrubbable, frame-preview extraction against an endless stream, and the live edge read as the end of the video. So the declared type decides, and it is known before the first frame rather than after it, because a channel that is down when you open it fails during initialize() and that is exactly the path that needs to know. Reconnection is the other half. A film either plays or is broken, so four retries a session is generous; a channel drops because a segment was late or a CDN failed over, and the only right answer is to open it again. Four in a two-hour evening meant the player gave up for good on something that was working seconds later. A broadcast now reconnects with a backoff, on errors a file would call fatal — a 403 on a live edge is usually a rotated token, and the next playlist fetch has the current one. Seeking is refused outright. The double-tap gesture reached the same seek as the scrub bar, and a tap landing outside the DVR window stalls the stream with no way back but leaving the channel. Channels also carry the headers their origin insists on, all the way to the player and into the pinned-card cache — a header-gated channel used to work once and 403 forever after being favourited.
It was reachable only through Profile, seven rows down a settings list. That is a fine place for a preference and the wrong place for a thousand channels: Live TV is content, and it belongs where the content is. Home grows a rail of channel logos above the catalogue sections. A rail rather than a link, because a channel is picked in a second and watched immediately — tapping a logo plays it, and the header still opens the full page for folders and search. Logo-forward and square, since a channel is recognised by its mark long before its name is read. It loads itself and renders nothing until it has channels, so a backend with no line-up — or no network — leaves Home exactly as it was rather than showing an empty shelf. Live TV also remains available as a bottom-bar tab; it has been in the registry all along, just not on by default.
AppColors.primary is a getter — the accent is a user setting — so it cannot appear in a constant expression, which is exactly what app_colors.dart says at the top. The LIVE dot's decoration claimed const and failed to compile.
The screen was a search box, a folder grid and a paged wall of logos. It told you a thousand channels exist and nothing about any of them, and the guide the backend had started returning was not read at all. Channels now carry their listing. A card shows what is on now, how far through it is, and what follows; a long press opens the channel with its schedule for the day. About 38% of the line-up has no guide, so every one of those paths is written to look deliberate without one rather than to show a gap where a title should be. Around that: categories and countries are one index rather than two competing navigations, pinned and recent channels lead, and the states that were bare — first load, loading more, no results, offline — are all drawn. Country names are in English, like the channel names beside them; a strip reading "O'zbekiston" over a grid of "Pluto TV Comedy" was two languages doing one job. The clock is the part worth being careful about. Times arrive as UTC and are converted on the way in, so an hour rendered is the viewer's own; progress is clamped at both ends so a stale payload reads as finished rather than as 140%; and a bar is only drawn for a slot between five minutes and six hours, because a feed padding a thin guide with a nine-hour block would otherwise show a bar that has not moved since breakfast.
kSozoUserAgent existed for a good reason — Cloudflare binds cf_clearance to the exact agent that solved the challenge, so one string everywhere is right. But the string it settled on was invented: `Chrome/125` on a device whose WebView is some other version entirely. A managed challenge does not only read the header. It compares what the header claims against what the engine behind it is — client hints, userAgentData, platform, version. Measured against animepahe.pw, which sits behind one: driving a real browser through the challenge, the agent that matched its true platform cleared in about four seconds, while every agent that misdescribed the platform sat at "Just a moment..." until the watchdog gave up. Same browser, same flags, same window size; only the header differed. So the value is read from the device's own WebView at startup and reused everywhere, with the old constant kept only as a fallback. The `; wv` token is dropped — it is the one edit that does not make the string less true. channel_sheet.dart is unrelated: showAdaptiveModal was used without its import, which failed analysis and would have failed the build. Co-Authored-By: Azamov X <108933534+professorDeveloper@users.noreply.github.com>
The sniffer keeps whatever url the page requested. For a player that asks for one rendition and never for the master, that pins playback to a ladder of one — the qualities exist, the page simply never asks for them, and the master sits under a name derivable from the variant's. So the extractor directive can now carry a `rewrite`: a regex the server sends and this applies to the sniffed url. No provider name reaches the app; the rule travels with the source, the same way blockHosts and urlPatterns already do. Verified before it is trusted. A derived url is a guess about someone else's naming, and a guess that 404s must not cost a user their playback, so the candidate is fetched and kept only if it answers as a playlist. One request, and only when a rule was sent at all. Co-Authored-By: Azamov X <108933534+professorDeveloper@users.noreply.github.com>
Two conflicts, both in files this branch already touched. `dart_fetch.dart` — master still declares the solve agent `const`. It cannot be one any more: the app now reads its User-Agent from the device's own WebView, so `kSozoUserAgent` is a getter and `final` is the only thing that compiles. `channel_sheet.dart` — master's copy calls showAdaptiveModal without importing it, which fails analysis. This branch added the import; the merge keeps it. Co-Authored-By: Azamov X <108933534+professorDeveloper@users.noreply.github.com>
Three conflicts, all resolved in master's favour, and none of them arbitrarily. `user_agent.dart` and `dart_fetch.dart`: this branch strips the `; wv` token from the device agent and solves Cloudflare challenges inline. Master reworked both a day later — "Make a Cloudflare solve mean a clearance was actually earned" and "Solve Cloudflare challenges outside the JS fetch handler" — and its comment names the older approach and rejects it: the engine really is an Android WebView, so a UA claiming plain Chrome is the mismatch a managed challenge looks for. Keeping this branch's side would have reintroduced what master had already moved past. `player_page.media.dart`: both hunks are pure master additions (`_expandMasterPlaylist` and its call site). The conflict was adjacency, not disagreement — this branch's side of both hunks is empty. Resolved hunk by hunk rather than by taking whole files, so everything git merged cleanly on this branch is kept.
…in screen The language lived in two places that never spoke to each other. `context.setLocale()` persists through easy_localization's own storage and is what the interface reads; `HiveService.saveLanguage()` is what the subtitle translator falls back to and what the backend is told for push copy. Only the settings screen wrote the second one, and nothing wrote it at startup — so a German phone showed a German interface while Hive still said `en`, and that user's subtitles and notifications quietly stayed English. Both writes now go through `AppLanguage.set`, and `syncFromDevice` repairs accounts already in the split state: on a first launch the resolved locale comes from the device, and Hive's own default is the string `'en'`, which is a real answer rather than an absent one, so nothing downstream could tell "English" from "never asked". `useFallbackTranslations` is enabled. Without it a key missing from one locale renders as the key itself — the user reads `profile.language_desc` where a sentence belongs. CI gates on the key check, so it should never fire; it is here because the day it does, English is a far better answer. The picker is a chip on the sign-in screen rather than a screen of its own. The app already resolves the device locale by itself, so a dedicated first-run language step would charge every user a step to serve the minority whose phone is set to something they do not want to read. The chip costs nobody a step, and the slides re-render in the chosen language immediately — which a picker placed after the introduction cannot do, because by then the introduction has been read in the wrong one. Registering the FCM token now carries the language up with it, keyed on the language as well as the token so switching language re-registers instead of being swallowed as a duplicate.
Eleven languages now, up from four. All seven new ones are Latin-script: the app bundles no font, so the system one covers them, and none needs the mirrored layout Arabic does. `pt`, not `pt-BR`, though the copy is written in Brazilian Portuguese. A region tag would have been the only two-part code in the set — a special case in the file names, in the check script, and in `kSubtitleTranslateLanguages`, which lists plain `pt` and would have silently dropped a `pt-BR` reader back to the default subtitle target. The copy is machine-generated and has NOT been reviewed by a native speaker. German and Dutch are the two markets this was asked for; treat reports about their wording as expected rather than surprising. `localization_test.dart` reads the locale list off disk instead of naming four files. A hard-coded list silently stops covering the locale nobody updated it for, which is the exact failure the file exists to catch. `check_translations.dart` now blocks only on `en`, `uz` and `ru` — the three the team writes. Blocking a PR on the other eight would not produce eight translations; it would produce eight copies of the English string pasted in to get the build green, which is the same debt with the tracking removed. They are reported instead, and the fallback added alongside means a gap renders as English rather than as raw dot-notation. `check_expansion.dart` ranks short strings by how much longer they got. Overflow is not random — it happens where the most-expanded string meets the tightest box — so the ranking names the screens worth opening in German instead of auditing all sixty. Five sites it found are fixed here: three retry buttons pinned to 156px that "Tentar de novo" does not fit, and two fixed-width label columns in the player and the source tester. The player's info labels were shortened in four locales rather than ellipsised, because "Almacenado por…" hides the very word that says which row you are reading.
Reported as the app freezing once the installed source list passes a couple of thousand. Two separate causes, and they compound. **The screen stopped responding.** `_remerge()` ran on every arriving leg, and `mergeSearchResults` rebuilds from scratch — it regroups every item of every leg collected so far and re-scores each one against the query, then sorts. Per arrival that is quadratic in the number of sources: at 2000 legs, tens of millions of item comparisons over a run, all on the UI isolate, plus 2000 full rebuilds of the results list. The engine's network work was already off-thread; this was not. Arriving legs are now coalesced into one merge per 250ms window, with an immediate flush when the run ends so the last legs are never stranded in an unflushed window. The test that guards it fails at 1000 notifications without the change and passes under 5 with it. **The run took over an hour.** Nothing capped the set. The pool is bounded at 5 with a 10s per-provider budget, so a run costs roughly `legs / concurrency * timeout` in the worst case — about 67 minutes for 2000 sources. A search nobody waits for is a search that did not happen. `planLegs` caps an all-source run at 60 legs, taken after the existing health ordering, so the ones kept are the ones most likely to answer. It is applied where the set is built rather than inside `search`, so the counters the UI shows — pending, answered, timed out — still describe the run that is really happening. A scope the user picked by hand is never trimmed: then the count is theirs. The summary line says which 60 of how many, because a number that quietly disagrees with the source list is worse than a long search. `SearchFanOut` is a narrow interface over the two methods a search surface actually uses. The engine reaches WebViews, a Dio client and the Mangayomi bridge; standing all three up to assert that legs are batched would have tested the fakes rather than the batching.
Reported as films downloading blurry with no way to pick a quality — on vidapi, which is worth naming because it explains why the setting could not be found. vidapi's `stream_urls` are servers, not qualities: same picture, different mirror, as its own comment says. So there is no 1080p entry anywhere in the provider's response to choose — the renditions live inside the HLS master playlist, one level down. The player already read them: it parses the master and builds its quality menu from it. The downloader parsed the same file and took whichever variant was listed first. Packagers commonly order a master lowest-bitrate-first, because a client is meant to start conservatively and adapt upward — so "first" was the 480p rendition of the stream the player was showing at 1080p. The download completed, sat in the list, and looked wrong, with nothing on screen to suggest a quality had been picked at all. The ranking now lives in `parseHlsVariants` and both callers use it. Keeping two copies is what let them disagree, and the subtle half is not obvious enough to survive being written twice: the packager's own name for a variant beats RESOLUTION, because a 2.40:1 film encoded at 1080p carries RESOLUTION=1920x800 and ranking on pixel height alone puts it below a true 1080p and labels it 800p. Nobody downloads a film to keep the smallest copy of it, so the best rendition is the answer rather than a new setting. The parser also splits on CRLF, which the downloader's did not — a master served with Windows line endings left a stray carriage return on every url it built.
…es, and update UI alignment
Reported as the provider picker being buried, and it was: from Profile it took Sources, then Active source, then the full providers page. Three screens for the thing this app asks people to do most often, and the last of them is really for installing extension repos rather than picking a source. The home screen already had the answer — a quick-switch sheet with the modes, the favourites, a filter once the list is long enough to need one, and the home screen still visible behind it. Its own comment records why it exists: the report before this one was "I have to go to settings every time I want to change source". It was just nailed to the top bar. So it moves out of `home_top_bar.dart` into its own file and both entry points open the same sheet. Building a second picker for Profile was the alternative, and two controls for one decision drift apart — this one already carries enough judgement (offline sources dropped, mode narrowing, the switch animation covering the reload) that a rebuild would have quietly lost some of it. Profile now shows two rows where it showed one, because they are two jobs: `Provider`, which names the current source and opens the switcher, and `Extension sources`, which goes to the management screen. The rule is the same everywhere now — a row that shows the current source is the row that changes it, including the one on the sources screen, which used to push the providers page. `home_top_bar.dart` drops from 1152 lines to 676. What is left is the top bar: the other buttons, the indicators, the shortcuts. Also localised `Mangayomi Sources`, a title that was still a Dart string literal — the extraction pass looked at layouts and `.text =` assignments, and this one was neither.
Three related defects, all from the same wrong assumption: that a provider's source order is already ranked. SourceLadder ranked by remembered quality, then isDefault, then backend order, documented as "already ranked". It is not — asilmedia and animefenix mark nothing default and list 360p ahead of 720p, so the ladder opened on the worst rendition. Resolution is now rule 3, parsed from the label; a label with no resolution in it scores 0 and keeps the backend order, so vidapi's "Server 1/2/3" is unaffected. detail_page's _pickMovieUrl had its own hand-rolled version of the same walk, with the same defect, feeding the downloader. It calls the ladder now. _expandMasterPlaylist could only be reached through _applyRewrite — a provider with a rewrite rule that verifies, and a source list of exactly one. vidapi has no such rule and returns three masters, so the sheet listed servers, the renditions inside them were never surfaced, and only the connection speed chose between 480p and 1080p. Playback now reads the current server's master once and inserts its heights after it, leaving that entry untouched as the adaptive choice; other servers expand when switched to.
A scan for string literals reaching a widget's visible slots found 60-odd that never went through .tr(): the whole app-updater flow, the CloudStream, Aniyomi and Mangayomi source pages, the repo-file import dialog, the log viewer, and a "FAQAT KO'RISH" badge sitting hardcoded in Uzbek on the providers page — which read as Uzbek to a German user. 82 keys across all 11 locales. Numeric interpolations, brand names and the font-preview pangram are left alone deliberately.
cross_search_controller_test imports it and it only resolved as a transitive dependency of flutter_test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.