Skip to content

Add NLZIET support#1934

Draft
oliv3r wants to merge 38 commits intoretrospect-addon:masterfrom
oliv3r:nlziet
Draft

Add NLZIET support#1934
oliv3r wants to merge 38 commits intoretrospect-addon:masterfrom
oliv3r:nlziet

Conversation

@oliv3r
Copy link
Contributor

@oliv3r oliv3r commented Feb 26, 2026

Functional description

This merge requests adds support for NLZIET for both live streams, VOD, profile support with device flow and username/password authentication.

Reasoning

Closes #645

Technical description

A new channel was added, and some plumbing fixed.

Tasks & Activities

  • Review some commits, not everything is perfect yet
  • Fix the 'jump back to start of stream
  • Investigate if we can change the buttons to actual buttons in the authentication screen
  • Endurance testing (had a bug where after watching a Live stream for a long time (65 minutes) changing channels took significant time due to failure to refresh token, should be fixed, but not sure yet
  • Testing of 'extra' channels, i don't have that type of account
  • Setup a NLZIET account specific for retrospect. When creating the 'free for the first 2 weeks' account, it is possible to run the guarded tests. canceling the account within those 2 weeks breaks streaming capability, but the account and its API's still work. That would need to be added to github for the pipeline to run real API tests.
  • Fix a bug on the first/last/continue shortcut which is not de-duplicating.
  • Validate IPTV support (haven't gotten it to work yet)
  • Add 'up-next' support
  • Fixup QR code inclusion
  • Test 'big list' categories (just forgot to look at it)

@oliv3r
Copy link
Contributor Author

oliv3r commented Feb 26, 2026

Big fat disclaimer, I am not a python coder, I know it very little. I will work more on it, but this was mostly vibe-coded. (do not shame me to much ;)

I do want some early review happening however to see if the general approach is alright, and get some testing feedback from those who dare. I've been using an earlier version (where it would just stream live tv) for 2 weeks now and that part is great. (I haven't updated that on our TV yet as that was enough for the WAF for now :p) all other testing I did locally on my laptop and through the included (almost 100) tests of the test suite. I haven't looked at the test code almost at all however.

@basrieter
Copy link
Collaborator

Those are a lot of changes! Perhaps too much for a PR for NL Ziet. But anyways, good stuff. I do have some remarks, which I will post in the PR.

One major this is that we should not include Python libs in other add-ons. So please see if you can use https://kodi.tv/addons/omega/script.module.qrcode/ for the QR Code stuff.

Another thing is that I advice you to use https://pypi.org/project/sakee/. That is my Kodi emulator that you can use to run an add-on without Kodi and get some simple ASCII results.

@oliv3r oliv3r marked this pull request as draft February 26, 2026 21:51
@oliv3r
Copy link
Contributor Author

oliv3r commented Feb 26, 2026

Those are a lot of changes! Perhaps too much for a PR for NL Ziet. But anyways, good stuff. I do have some remarks, which I will post in the PR.
Please do; but realize even I am not happy with it yet ;)

Most changes are scafolding changes needed for NLZIET, we can do those in seperate MR's to get into the framework, but then they'd have no user yet. Either way is fine.

I've tried to split my commits into 'somewhat smallish' commits from a feature point of view, to avoid having 'one huge fully featured with everything' commit however.

One major this is that we should not include Python libs in other add-ons. So please see if you can use https://kodi.tv/addons/omega/script.module.qrcode/ for the QR Code stuff.

So far as I could see, this is not an official addon, so we cannot depend on it, and will have to ask users to manually install it. There's other ways, with pure python implementations to do QR codes of course. But as I stated in the commit, the QR-code IS script.module.qrcode. So whatever approach you suggest we can do.

Another thing is that I advice you to use https://pypi.org/project/sakee/. That is my Kodi emulator that you can use to run an add-on without Kodi and get some simple ASCII results.

I've just ran whatever unittest.yaml did, which is installing the requirements.txt (which I would assume uses sakee?) and running the (full) pytest suite.

@oliv3r oliv3r changed the title [DRAFT] Add NLZIET support Add NLZIET support Feb 26, 2026
@oliv3r
Copy link
Contributor Author

oliv3r commented Feb 26, 2026 via email

Copy link
Collaborator

@basrieter basrieter left a comment

Choose a reason for hiding this comment

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

Ok, looks like a lot of nice work! I will need to play around with it a bit, especially the device login.

For now I went through some of the (none-channel related) changes and left comments.

One thing (besides the QR stuff) is that you are making a lot of changes outside of adding NL Ziet. I understand these changes (some are nice, like the profile dependent search history). However, they clutter the PR and make it difficult to review and test.

So, even thought I understand your urge to update some 20 year old code, it would be better to separate this from the NL Ziet channel.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if this will work, as we use Weblate to translate stuff. Retrospect is set up to use the en-gb as the main language and Weblate will them try to fill the rest. But we will see what happens.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, as a native dutch/german speaker I figured i'll add those 3 languages as well. I saw we used weblate for a lot of the other stuff; but I also got missing/empty strings when testing it on my system, so that was a little annoying :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

So in general these tests are great. However....(there is always a but).....

I don't use tests for code testing at all (bare minimum) like you are doing here. These tests are really great stuff, but the tests in Retrospect are used to actually call API end-points and see if they still provide the API messages that work for Retrospect. So I abuse the unit tests to do integration testing. That is way the UriHandler is not mocked at all: I actually need it to do the http(s)-calls.

Copy link
Contributor Author

@oliv3r oliv3r Feb 27, 2026

Choose a reason for hiding this comment

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

yeah, I wasn't sure how to deal with that. I test both actually, but NLZIET requires a payed subscription. From early testing, after having an account, it may work without actually paying for it, just leaving the account 'on', so then we can still do some of the testing.

I didn't want to hammer the API, which is why I added both, real api tests for authentication, and mocking of everything else. This way, I could add a lot of different tests. There is one real API test that also validates the real API against the mocks.

Also, maybe more importantly, to be able to work on this one needs NLZIET credentials to do testing, which even account holders, may not want to do (locally), so being able to mock, allows for some basic tests to be performed. also mocking the API's at least gives us control what they will return, the real API will often return different content for certain endpoints of course. So I figured best of both worlds?

If the problem is the conftest thing, I can also make it part of only the NLZIET mocks. I just didn't know what the problem was and how to deal with it :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I understand, as long as the mocking is not interfering with other tests. So you cannot mock the UriHandler or Logger for on a global scope.

Copy link
Collaborator

Choose a reason for hiding this comment

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

So basically what you do here, and in the other test, I usually combine. Because writing such extensive tests, just takes up too much time. My philosophy is, that if the API changes and breaks 2 things can happen:

  • Parsing results on 0 results, or
  • The converting from API to results fails

Both are covered in my simple tests like

    def test_tv4_tv_shows(self):
        url = "https://www.goplay.be/programmas/"
        items = self._test_folder_url(url, 20)

Which basically uses the TV4 channel (in this case) and just runs a parse off the given URL.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand your philosophy, but I avoided it for the two previously mentioned reasons. a) other contributors may not have an account they want to use/sacrifice because of FUD on getting banned; being able to run tests locally without an internet connection :)

Having said that, I think it's trivial to do the same that we did with the authentication tests, run against the REAL API when username/password are set, against the MOCKS if not. I think doing mocks doesn't add a lot, I'll update this sunday as well.

Copy link
Collaborator

Choose a reason for hiding this comment

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

So this basically disables the UriHandler for all tests and it breaks all tests. So this won't work for Retrospect as we need actual https responses.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, fair enough; as mentioned above. i'll fix it sunday :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

So, this file get's auto generated from the plugin.video.retrospect/resources/data/settings_template.xml. So the only thing you need to do is to configure your settings in the chn_nlziet.json, clear Python caches and run Retrospect once. It will generate the correct settings.xml.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ahh, good to know, I wasn't sure how this worked, or rather, was confused by the committed settings.xml. Why are there settings committed? Should I commit them at all?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please don't update the urihandler.py. It is one of the core modules and should contain all stuff you need. If you really need something, please make the impact as minimal as possible.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please use the RETROSPECT_STDOUT_LOGGING to disable logging, not via these changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

these are old debug commits; as I said, I still need to review most of the code changes :p

@basrieter
Copy link
Collaborator

I also noticed that if you have a PVR add-on installed, the pop-up for NLZiet appears, even when NPO Start is active:

image

return None


class DeviceAuthDialog(xbmcgui.WindowDialog):
Copy link
Collaborator

@basrieter basrieter Feb 26, 2026

Choose a reason for hiding this comment

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

I don't like mixing the xbmcgui.WindowDialog in Kodi plugin add-ons. It just looks ugly and never matches the actual skin users use. So I dialogs should be the most simple ones, where the skinning is part of the main skin, not Retrospect.

Can it not work with xbmc.executebuiltin('ShowPicture("https://example.com/image.jpg")')?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have no idea :D i was presented with three options, either the programmatically, like now, the standard dialog which did not allow for the needed modification of adding a QR code, or using a 'XML based dialog' which meant adding a skin, which I didn't want to do. I was under the impression that the programmatical gui would just use the skin, I was wrong, so I'll try this more, but will the showpicture work ontop of a dialog box? idk.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So after digging deeper into this XML based dialog, the changes make a lot of sense to use that. I refrained from using it as we didn't have XML dialogs yet, but I also see it's quite trivial and works with skins, not against it. I'll fix that asap.

@oliv3r
Copy link
Contributor Author

oliv3r commented Feb 27, 2026

Ok, looks like a lot of nice work! I will need to play around with it a bit, especially the device login.

For now I went through some of the (none-channel related) changes and left comments.

One thing (besides the QR stuff) is that you are making a lot of changes outside of adding NL Ziet. I understand these changes (some are nice, like the profile dependent search history). However, they clutter the PR and make it difficult to review and test.

So, even thought I understand your urge to update some 20 year old code, it would be better to separate this from the NL Ziet channel.

Every change presented to the framework, was done to support NLZIET (with device flow ;p)

the profile dependent search history is truly just a 'nice to have' ;)

So what do you recommend? just open single MR's for the trivial stuff, do you want to cherry pick some of them into master when you think they are good? Anything is fine by me; but the gross of the changes are needed for this new channel to work reliably.

@oliv3r
Copy link
Contributor Author

oliv3r commented Feb 27, 2026

I also noticed that if you have a PVR add-on installed, the pop-up for NLZiet appears, even when NPO Start is active:
image

interesting, this I will have to check why this is, right now, i think the pop-up comes when the channel gets loaded and we detect no configured session, not sure if it's related to what you mention though. I'll have to think what would work best, only when entering the NLZIET channel list and we're not being authenticated.

@powermarcel10
Copy link

Great work! I was looking for this for so long! 😀 So I'm happy to test. Iptv manager is not working yet right? Would be nice to use it to have some catchup options and watch stuff back from the epg. Is that something you're working on?

Also, which branch should I test? The review version? Or the other one.

@powermarcel10
Copy link

powermarcel10 commented Mar 3, 2026

With the latest version I'm getting this error; "De afhankelijkheid op script.module.qrcode versie 4.7.2 kan niet worden voldaan."

Any idea?

@basrieter
Copy link
Collaborator

basrieter commented Mar 3, 2026

@oliv3r Please ping me if you need help or if I need to review stuff. I could also add you to the Retrospect Slack if that makes live easier?

@oliv3r
Copy link
Contributor Author

oliv3r commented Mar 6, 2026

With the latest version I'm getting this error; "De afhankelijkheid op script.module.qrcode versie 4.7.2 kan niet worden voldaan."

Any idea?

shitty translation of 'missing script.module.qrcode' addon :) should work fine without it btw. I marked the dependency as optional, so i think during addon installation/update it will ask you 'do you want ot install script.module.qrcode' and if you say no, the code will still work just fine.

@oliv3r
Copy link
Contributor Author

oliv3r commented Mar 6, 2026

@oliv3r Please ping me if you need help or if I need to review stuff. I could also add you to the Retrospect Slack if that makes live easier?

Yes please! I'll figure that would.

I've changed my approach slightly. I will keep pushing my (functional) WIP stuff onto this branch, it will be messy, at times. but the idea is, that 'proper things' will get their own merge requests, and we should see the number of patches shrink. this MR will then just have the 'final feature' so when we merge this, then nlziet will be 'done' for now from my point of view :)

@oliv3r
Copy link
Contributor Author

oliv3r commented Mar 6, 2026

Great work! I was looking for this for so long! 😀 So I'm happy to test. Iptv manager is not working yet right? Would be nice to use it to have some catchup options and watch stuff back from the epg. Is that something you're working on?

Also, which branch should I test? The review version? Or the other one.

I missed your comment; but I keep pushing my 'work in progress' to this branch. Right now, I have working: livetv; Video on Demand, multiple profile support; iptv manager integration with 'catch-up' and watch-ahead, HOWEVER! Kodi does NOT support watch ahead sadly. so for that to work, we need to fix kodi.

@basrieter
Copy link
Collaborator

Great work! I am fixing Vier (Play.tv) at the moment and reviewing your smaller PR's.

@basrieter
Copy link
Collaborator

@oliv3r if you rebase on 1921-cant-get-vier-to-work you should have working unit tests.

oliv3r and others added 7 commits March 7, 2026 10:50
Container.Update triggers a separate plugin instance that races with
endOfDirectory. Slow API responses cause Kodi to render the stale
search-history page instead of the actual results.

Run the search directly in the keyboard branch so results are rendered
in the same plugin instance.

Known issue: empty needle is passed through, so refreshing search
results triggers the keyboard pop-up instead of re-running the query.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
Empty folder results previously showed an Error notification regardless
of the configured empty_list_behavior setting, something not considered
to be an error. Now only the 'error' behavior triggers an Error notification;
all other modes use Info.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
Without SORT_METHOD_UNSORTED as default, Kodi sorts alphabetically.
This reorders pinned items (search, explore pages) meant to stay at
fixed positions and shuffles live channel listings out of EPG order.

Make SORT_METHOD_UNSORTED the default sort method when any pinned
or live items are present, matching the existing search behavior.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
The DRM warning dialog is unnecessary when inputstreamhelper confirms
that InputStream Adaptive and the Widevine CDM are installed. Fall
through to the existing warning if the helper is unavailable.

This effectively makes the show_drm_paid_warning setting a no-op when
Widevine is present. A future refactor could replace the setting
entirely with a pop-up only when Widevine is not detected.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
There are currently no tests for the PATCH http method where the others
do have them. Add it so we can refactor this in the future more easily.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
The if/elif chain dispatched on body type first, with the HTTP method
buried inside POST branches, making it harder to extend with new methods.
Restructure to resolve the effective method up front, then dispatch with
flat independent if blocks in natural HTTP order (GET, POST, PATCH).

When no explicit method is passed but body arguments are present, the
request is silently promoted from GET to POST. This preserves existing
behavior relied on by existing callers. Ideally callers should specify the
method explicitly and the handler should reject ambiguous requests.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
The HTTP request handler supported GET, POST, and PATCH but not
DELETE. Add it to be more compliant with the HTTP specification, but
also so that others can make use of it in the future.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
oliv3r and others added 26 commits March 7, 2026 10:50
Add up to three playable shortcuts before the season folders:
Continue Watching (resume-aware), Most Recent Episode, and First
Episode.

When Continue Watching points to the same episode as First Episode
(i.e. the series has not been started), the Continue item is omitted.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
Implements create_iptv_streams() and create_iptv_epg() for IPTV
Manager support. Provides all NLZIET live channels and 6 days
of EPG data (3 past + 3 future). Replay-enabled programs include direct
playback links in the EPG grid.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
Populate tv_show_title on episode MediaItems so service.upnext can show
the correct series title in its 'Up Next' popup.

The series title is extracted from the /v8/series/{id} detail response
in the existing extract_series_data() preprocessor and stored in each
season FolderItem's metaData under 'nlziet:series_title'.

A new extract_series_title() preprocessor on the /v9/series/ parser
reads that key from self.parentItem.metaData and caches it on the
channel instance.  create_episode_item() then uses it to set
item.tv_show_title, which is forwarded to service.upnext via the
existing VideoAction.__get_up_next_data() framework method.

No new framework code is required; the trigger logic in videoaction.py
already fires automatically for any non-live episode when service.upnext
is installed.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
For each algorithm test in TestEpisodeBoundaryDetection there is now a
corresponding integration test in TestNLZietChannelLive (auto-inherited
by TestNLZietChannelMocked so it runs without credentials).

Tests cover the three key real-world orderings from HAR recordings:
- Flikken Maastricht S18: descending, clean broadcastAt (A1 / A13)
- De Luizenmoeder S1: descending, non-monotonic re-broadcast dates (A1 / A10)
- Fawlty Towers S1: ascending, clean broadcastAt (A1 / A6)

Changes:
- nlziet_mocks.py: Added three HAR-derived episode fixtures (FM S18,
  Luizenmoeder S1, Fawlty S1) and MOCK_EPISODES_BY_SEASON lookup dict;
  extended dispatch() to handle /v9/series/{id}/episodes endpoint.
- test_chn_nlziet.py: Added 6 boundary tests to TestNLZietChannelLive
  (_boundary_eps helper + first/recent assertion for each fixture).

Co-authored-by: Claude Sonnet 4.6
Signed-off-by: Oliver Bramley <oliver@bramley.dev>
Signed-off-by: Your Name <you@example.com>
Kodi generates settings.xml from the template only when a channel is
first installed (no __pycache__). Since chn_nlziet.json was added after
the initial channel discovery, the settings were never included.

Commit the generated settings.xml directly so the 7 NLZIET channel
settings are available immediately without requiring a fresh install:
- nlziet_device_setup (device flow setup action)
- nlziet_username / nlziet_password / nlziet_password_set
- nlziet_log_off (logout action)
- nlziet_select_profile
- nlziet_live_start_offset

Visibility indices for all other channels are shifted accordingly.

Co-authored-by: Claude Sonnet 4.6
Signed-off-by: Oliver Bramley <oliver@bramley.dev>
Signed-off-by: Your Name <you@example.com>
Replace __fetch_live_genres() (20 API calls on every EPG refresh) with a
lazy, queue-based enrichment system that populates descriptions and genres
incrementally — 10 items per 30-second cycle — without blocking the initial
EPG load.

## Architecture

### epg_enrichment.py (new)
Module-level helpers shared by the channel and the background service:
- load/save_detail_cache(): JSON cache in LocalSettings with 3-day TTL pruning
- load/save_enrich_queue(): ordered work list in LocalSettings
- build_enrich_queue(): builds priority-sorted queue from all programmes,
  excluding already-cached items; ordering: now → future (soonest first) →
  past (most-recent first, idle backfill only)
- fetch_and_cache(batch, headers): calls /v9/item/detail/ for ≤10 items,
  extracts description + genres[0].name, updates cache

### api.py
Added EPG_DETAIL_CACHE_KEY, EPG_ENRICH_QUEUE_KEY, EPG_ENRICH_BATCH_SIZE=10,
EPG_CACHE_TTL_DAYS=3.

### chn_nlziet.py — create_iptv_epg()
- Applies description+genre from cache to all EPG items on every refresh
- After building the full EPG, rebuilds the enrich queue (uncached items only)
- Removed __fetch_live_genres() — no longer needed

### retroservice.py
Replaced the one-shot script with a real xbmc.Monitor loop (waitForAbort(30)).
Each 30-second tick calls _drain_epg_enrich_queue():
  1. Pop 10 items from the queue
  2. Fetch item/detail (reuses NLZIETOAuth2Handler for auth headers)
  3. Update cache; save remaining queue
  4. If any new data: set service.iptv.manager last_refreshed=0 → triggers
     IPTV Manager to regenerate epg.xml with fresh descriptions

## Cold-cache behaviour
- Cycle 1 (~30s): ch0-ch9 get descriptions in the 'now' column
- Cycle 2 (~60s): ch10-ch19 'now' described — full 'now' column done
- Cycles 3-N: next/future slots enriched, soonest first
- Full ~60-item view window populated in ~3 min; warm cache = 0 API calls

Co-authored-by: Claude Sonnet 4.6
Signed-off-by: Your Name <you@example.com>
…daptive backoff

- Add __load_appconfig() helper: caches /v7/appconfig in LocalSettings
  (TTL = APPCONFIG_CACHE_TTL = 300s); logs warning if isUpdateRequired
- Add __check_app_blocked(): on isAppBlocked=True shows appBlockedReason
  to user (Kodi notification) and signals caller to abort API work
- Add __signal_iptv_manager(delay): always signals IPTV Manager after
  create_iptv_epg with an adaptive delay (30s – 600s) so EPG stays live
  forever; delay grows 30s per empty cycle, resets when work arrives
- Add programme-location cache (nlziet_epg_progloc_cache, TTL 300s):
  during 30s drain cycles only the 10 item/detail calls hit the network
  instead of ~280 redundant programlocation fetches
- Use appconfig.liveStreamRestartStartPadding (180s) as base for
  live_offset; user setting nlziet_live_start_offset becomes a +-120s
  adjustment relative to the server value (range updated accordingly)
- epg_enrichment: add load/save_progloc_cache, load/save_backoff_cycles,
  compute_signal_delay helpers; api.py: new APPCONFIG_CACHE_KEY,
  APPCONFIG_CACHE_TTL, EPG_PROGLOC_CACHE_KEY, EPG_BACKOFF_CYCLES_KEY
- CI test_20_appconfig_status: live test asserting isAppBlocked=False
  (hard fail) and isUpdateRequired=False (DeprecationWarning)
- 11 new unit tests covering progloc cache and backoff helpers

Co-authored-by: Claude Sonnet 4.6 (claude-sonnet-4.6)
Signed-off-by: Your Name <you@example.com>
Addon.refresh() in service.iptv.manager writes last_refreshed after
create_iptv_epg returns, clobbering any signal we write from within
create_iptv_epg. The previous __signal_iptv_manager() approach was
therefore a no-op: IPTV Manager would not refresh again for 24h.

Fix: __signal_iptv_manager() now writes a plain-text file
(nlziet_epg_signal_at) containing the target Unix timestamp.
retroservice.py polls every 30s; when the time arrives it writes
last_refreshed="0" to IPTV Manager — after Addon.refresh() has
already exited — and removes the file.

retroservice.py also handles the cold-start case: if settings.json
has no nlziet_epg_progloc_cache (fresh install / first run), a
signal file is written 5s after service startup so the first IPTV
Manager refresh is triggered immediately instead of waiting 24h.

Co-authored-by: Claude Sonnet 4.6
Signed-off-by: Oliver Kuckertz <oliver.kuckertz@mologie.de>
Signed-off-by: Your Name <you@example.com>
Add a boolean channel setting (default: true) that hides channels
requiring an additional subscription package from both the IPTV stream
list and the EPG.

The API exposes this via missingSubscriptionFeature on each channel
entry: null = included in current subscription, non-null = extra
package required (e.g. ExtraChannelPackage1).

- create_iptv_streams: skip channels with missingSubscriptionFeature != null
  when setting is enabled
- create_iptv_epg: accumulate subscribed channel IDs during the stale
  (fresh API fetch) pass and persist them in the progloc cache as
  'subscribed_channels'; on cache-hit passes, reuse the stored set to
  filter EPG entries without an extra API call
- chn_nlziet.json: add order-8 bool setting nlziet_epg_subscribed_only
- resources/settings.xml: add corresponding setting entry (visible eq -11)
- strings.po #30630: 'Only show subscribed channels'

Co-authored-by: Claude Sonnet 4.6 (claude-sonnet-4.6)
Signed-off-by: Your Name <you@example.com>
…uplicates

epg_enrichment.py had its first block of functions duplicated verbatim
starting at line 253. Because Python uses the last definition, build_enrich_queue
and fetch_and_cache were the simpler broken versions. Removed the duplicate.

Logging additions:
- retroservice.py: _log() helper (xbmc.log); logs addon_data/signal path at
  init, tick counter, signal file found/remaining/fired, IPTV Manager signal
  success/failure, initial trigger write
- create_iptv_streams: entry + returned/skipped channel counts
- create_iptv_epg: entry with days/stale, drain step, per-day fetch vs cache,
  total channels+programmes collected, enrich queue saved count
- __signal_iptv_manager: signal path + delay; exception as WARNING
- __load_appconfig: cache hit (age) vs fresh fetch
- load_progloc_cache: hit (age, date keys) vs stale/absent
- build_enrich_queue: now/future/past breakdown with cached count
- fetch_and_cache: batch size, per-item fetch, enriched/skipped counts

Co-authored-by: Claude Sonnet 4.6
Signed-off-by: Oliver Kuckertz <oliver.kuckertz@mologie.de>
Signed-off-by: Your Name <you@example.com>
The progloc cache (42 MB) and enrich queue (2.5 MB) were stored in
settings.json via AddonSettings, causing Kodi to throw repeated
'EXCEPTION: Invalid setting type' errors on every PVR manager reload.

Changes:
- Move progloc cache to nlziet_epg_progloc.json (file I/O, no size limit)
- Remove enrich queue persistence entirely: the queue is now built and
  drained within the same create_iptv_epg call, since build_enrich_queue
  already excludes items in the detail cache, making the saved position
  implicit
- Remove cdata from progloc cache entries (was the bulk of the 42 MB);
  __create_replay_item now takes asset_id + title directly
- Slim image field to store only the landscapeUrl string instead of the
  full image dict
- Add _CACHE_DIR override in epg_enrichment.py for test isolation
- Remove EPG_ENRICH_QUEUE_KEY from api.py (no longer used)
- Update tests: TestQueueIO removed; TestProlocCache uses temp dir

Co-authored-by: Claude Sonnet 4.6
Signed-off-by: Oliver Klomp <info@retrospect.tv>
Signed-off-by: Your Name <you@example.com>
Future programmes tagged with WatchInAdvance in the progloc API now get a
playable stream URL in the EPG, allowing users to watch upcoming content
before its scheduled broadcast time.

Implementation:
- Store is_watch_ahead flag as field [11] in progloc cache entries
  (backward-compat: old caches default to False)
- Add stream URL block for watch-ahead items in create_iptv_epg
- Prefix EPG description with localized 'Watch in advance' label
- Add string #30631 to en_gb and nl_nl translations
- Add SubscribedOnly (#30630) and WatchInAdvance (#30631) to languagehelper

Test improvements:
- Fix EPG test fixtures to include contentItemId and assetId fields
- Add cache isolation (temp _CACHE_DIR) to prevent inter-test leakage
- Increase mock side_effect counts for 15-day EPG window
- Add symlink for addon path assertion
- Add test_iptv_epg_watch_ahead_gets_stream
- Add test_iptv_epg_future_without_watch_ahead_no_stream

Co-authored-by: Claude Opus 4.6 (high)
Signed-off-by: Your Name <you@example.com>
A crash in any single channel's create_iptv_streams() or
create_iptv_epg() was killing the entire IPTV Manager response,
resulting in an empty playlist and no EPG data for all channels.

Wrap each channel's contribution in try/except so one broken
channel only loses its own streams/EPG, not everyone else's.

Co-authored-by: Claude Opus 4.6 (high)
Signed-off-by: Your Name <you@example.com>
- Add GENRE_MAP dict translating Dutch NLZIET genres to
  kodiDvbGenres.xml canonical English strings
- Ship resources/data/pvr_genres.xml mapping English canonical
  genre text to DVB hex colour codes
- retroservice.py installs pvr_genres.xml into pvr.iptvsimple's
  genreTextMappings directory on startup (version-tracked)
- Fix: movie genre fallback now fires when enrichment cache exists
  but has no genre (was only firing when cache entry was absent)
- Tests: genre translation, movie fallback, unknown passthrough,
  GENRE_MAP ↔ pvr_genres.xml consistency check

Co-authored-by: Claude Opus 4.6 (high)
Signed-off-by: Your Name <you@example.com>
When fetch_and_cache hits a network error (ConnectionError / DNS
failure), UriHandler.open() raises rather than returning None. This
exception was uncaught, propagating through create_iptv_epg and causing
NLZIET to contribute an empty dict to the IPTV Manager EPG response.

Fix: wrap UriHandler.open() in a try/except; on any exception, log a
warning and break out of the batch loop early (all remaining items in
the same batch will fail the same way, so aborting saves time). The
rest of create_iptv_epg continues normally and returns whatever EPG
data is already in the progloc cache.

Co-authored-by: Claude Sonnet 4.6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Your Name <you@example.com>
Replace the standalone inputstream.adaptive.live_offset property (which
ISA never read) with the manifest_config JSON blob approach introduced by
the local ISA patches (0003 + 0004 in ~/src/archlinux/kodi-addon-inputstream-adaptive).

The patched ISA adds a virtual +1 chapter to all live DASH streams, which
activates Kodi's |< and >| buttons:
- |< (SeekChapter 1)       → seeks to m_liveOffset seconds from stream start
                             = start of current programme (~180 s buffer)
- >| (SeekChapter virtual) → iterates enabled streams, seeks to getMaxTimeMs()
                             = true live edge

On unpatched ISA, manifest_config already exists; unknown keys emit a
LOGERROR but are otherwise ignored, so this change is harmless there.

Co-authored-by: Claude Sonnet 4.6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Your Name <you@example.com>
Server time (/v7/currenttime):
- Add API_V7_CURRENT_TIME constant to api.py
- Add Channel.__get_server_time(): fetches /v7/currenttime (ms float),
  converts to seconds; falls back to time.time() on any error.
  Intentionally private per-channel — other channels use their own
  time reference in create_iptv_epg.
- Replace 'now_ts = time.time()' in create_iptv_epg with
  'now_ts = self.__get_server_time()' so live/replay/watch-ahead
  boundary decisions use the authoritative NLZIET server clock.

Per-channel genre XML:
- Add channels/channel.nlziet/nlziet/genres.xml listing only the
  genres actually present in GENRE_MAP. retroservice discovers and
  merges these on top of the base pvr_genres.xml; channel entries
  override same-genreId entries from the base.

Co-authored-by: Claude Sonnet 4.6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Your Name <you@example.com>
…tance

_PVR_GENRES_VERSION bumped to 2 to trigger reinstall on upgrade.

_merge_genre_xmls():
- Parses base resources/data/pvr_genres.xml
- Walks channels/**/genres.xml; channel entries override same-genreId
  entries from the base (channels control only genres they emit)
- Builds a merged genres.xml with version header

_set_xml_setting():
- String-level XML patcher that updates a <setting id=...> element
  (strips default='true' attribute), or appends the element when absent
- Preserves file formatting

_configure_pvr_instances():
- Scans all instance-settings-N.xml in pvr.iptvsimple's profile dir
- For instances whose m3uPath contains 'service.iptv.manager' (i.e. the
  Retrospect IPTV instance), sets useEpgGenreText=true, genresPathType=0,
  genresPath to the installed genres.xml path

_setup_pvr_genres():
- Version-checks merged genres.xml before reinstalling
- When already at current version: still calls _configure_pvr_instances
  (idempotent — no-op if already correct) so fresh pvr.iptvsimple
  installs get configured on the next service start

pvr_genres.xml: update comment to version 2, clarify role as base file.

Co-authored-by: Claude Sonnet 4.6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Your Name <you@example.com>
_configure_pvr_instances() now also sets catchupEnabled=true and
catchupOnlyOnFinishedProgrammes=false in the instance XML.  Previously
these were only set in the global addon settings via configure(), but
per-instance settings take precedence and the instance file had
catchupEnabled=false (the pvr.iptvsimple default).

Without catchup enabled at instance level, clicking a future EPG entry
with a catchup-id always showed 'Record or Switch to Live' instead of
offering to play the watch-ahead stream.

Bump _PVR_GENRES_VERSION to 3 to trigger re-application on next startup.

Co-authored-by: Claude Sonnet 4.6
Signed-off-by: Oliver Klee <klee@oliver>
Signed-off-by: Your Name <you@example.com>
retroservice: add _has_epg_programme_data() that scans epg.xml for
any <programme> element. On the first tick, if the progloc cache exists
(has_epg_data=True) but the EPG file has no programme entries — which
happens when the file was last written during a network outage — call
_iptv_manager_signal() immediately to force service.iptv.manager to
re-run create_iptv_epg and repopulate the file.

chn_nlziet: guard __get_server_time() against implausible server
responses. If the server-reported time differs from the local clock by
more than 300 s (5 minutes), fall back to time.time(). This covers
cases where the API returns malformed data, an unexpected payload
(e.g. seconds instead of milliseconds), or a zero/future timestamp —
the delta will naturally exceed the threshold, so no explicit format
check is required.

Co-authored-by: Claude Sonnet 4.6
Signed-off-by: Oliver Klee <klee@oliver>
Signed-off-by: Your Name <you@example.com>
Future programmes can have both is_replay=True (will be replayable
once aired) and is_watch_ahead=True (playable now in advance). The
'and not is_replay' guard excluded these, so Brugklas never got a
catchup-id in the EPG.

The time-based guards already make the two branches mutually exclusive:
- is_replay fires on e_ts <= now_ts (programme has ended)
- is_watch_ahead fires on s_ts > now_ts (programme hasn't started)
There is no overlap, so 'not is_replay' is unnecessary and wrong.

Co-authored-by: Claude Sonnet 4.6
Signed-off-by: Oliver Klee <klee@oliver>
Signed-off-by: Your Name <you@example.com>
…ven refresh

- OAuth2Handler.__init__ reads _access_token, _refresh_token, _expires_at
  from AddonSettings once at construction; no per-call settings I/O
- _store_tokens() updates both settings and instance vars atomically
- _do_token_refresh(): unconditional internal refresh (single _ so subclass
  can override); base uses refresh_token grant
- refresh_access_token() -> bool: no-op when > 300s from expiry (_REFRESH_MARGIN),
  calls _do_token_refresh() otherwise; returns True/False instead of raising
- get_valid_token(): pure getter returning self._access_token — no side-effects;
  callers that need a fresh token must call refresh_access_token() first
- active_authentication(): calls refresh_access_token() before get_valid_token()
- _clear_token_settings(): clears both settings and instance vars

NLZIETOAuth2Handler:
- __init__ caches _id_token from settings
- _do_token_refresh() override: silent re-auth via id_token (web flow) or
  refresh_token grant (device flow); calls super()._do_token_refresh() for latter
- 401 fallback in list_profiles() calls _do_token_refresh() directly
  (force-refresh, bypasses expiry check)
- _store_tokens() and _clear_token_settings() maintain _id_token in sync

chn_nlziet:
- __set_auth_headers(): refresh_access_token() then get_valid_token()
- create_iptv_streams() + create_iptv_epg(): refresh token on every heartbeat
  cycle so long IPTV Manager sessions (4+ hours) never hit stale tokens

Tests (all 55 pass, 18 skipped = live-credential tests):
- Updated test_06, test_16: set _expires_at on instance (not just settings),
  call refresh_access_token() explicitly
- Updated test_refresh_no_tokens: assertFalse() instead of assertRaises(ValueError)
- New test_refresh_access_token_noop_when_valid: monkey-patch verifies
  _do_token_refresh is NOT called when token is fresh
- New test_get_valid_token_is_pure_getter: verifies no refresh side-effect
- New test_instance_vars_loaded_from_settings_at_init: verifies all three
  instance vars populated from settings at construction

Co-authored-by: Claude Sonnet 4.6
Signed-off-by: Oliver
Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 7, 2026

@oliv3r
Copy link
Contributor Author

oliv3r commented Mar 17, 2026

just dropping a quick status update; while this branch may have not seen any pushes for a while; i've been busy with it (and suffering 'trying to do too much' ... also, there's one significant flaw in my approach, that I have no fixed. I'll try to push again here soon, and move this forward.

@basrieter
Copy link
Collaborator

Thanks for the update. I just merged some of the other PR's for you.

Let me know if there are any other things I can help you with.

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.

NLZiet addition to retrospect

3 participants