Can you try shutting down Channels DVR and see if the tuners release? I ran into this once a few days ago and it seemed as though Channels was keeping the connection open.
ADBTuner tries to share tuners so that resume behavior is expected.
Can you try shutting down Channels DVR and see if the tuners release? I ran into this once a few days ago and it seemed as though Channels was keeping the connection open.
ADBTuner tries to share tuners so that resume behavior is expected.
I'm seeing this in the log, everytime I start the container. Using stable tag. Is this anything to be concerned about?
ERROR [alembic.util.messaging] Can't locate revision identified by 'c9f3aa41b7e2'
FAILED: Can't locate revision identified by 'c9f3aa41b7e2'
It looks like you tried the development version and then rolled back to stable? You can ignore that message for now. It will go away whenever you upgrade to a newer version in the future.
I pushed a small update to the stable build (20260723-1) this morning. It fixes a rare crash that could occur if an Android device goes offline (ex. reboots) in the middle of a tuning operation.
I also pushed an update to the development version (20260723-2) that fixes a bug that could cause the configuration clone feature to fail.
I pushed a new :development build this morning (20260724-10).
This build adds a long overdue Backup & Restore feature that can be used to back up your entire ADBTuner configuration (tuners, channels, configurations, etc.) into a single file.
Important Note
As part of this change, custom channel configurations are no longer stored on the filesystem. Any channel configuration files stored on the filesystem are automatically moved to the app's database during a one-time process on app startup.
Note: For now, these JSON channel configuration files are being left in place to allow users to roll back to the stable build if they wish. However, any configuration changes made in the development build prior to rolling back will be lost.
The web interface and API provide tools to import and export configurations individually or in bulk.
Additional Notes
Channel configurations can no longer be deleted if configured channels link to that configuration. This prevents the silent breakage that occurred before. This includes channel configurations that are being synced from a remote repository. The entire repository sync will be blocked if that sync would break a configured channel.
Anyone using the API or building integrations with ADBTuner should be aware that there is more type checking and validation in place now. This applies to API queries and the content within configurations. Configuration filenames don't matter, but the content within should all be valid.
Some additional API endpoints were added. Please review /api/docs for more info. Since the admin frontend now uses this API you should be able to do anything the admin interface can do via the API.
I did take a look at OliveTin, Project One-Click, and HDMI Encoder Native Apps and added some shims to make sure the existing integrations will continue to work. I apologize if this change causes any issues or makes any additional work for anyone.
Thanks everyone for testing and please report any issues. Thanks!
Hey turtletank! I wanted to thank you for those "timed keep active commands". They ended up being the lever I needed to pull to get my Osprey fleet working again. I was about to give up on Channels completely and just use an Osprey box natively because I could not for the life of me figure this out. What ended up solving it was Opus 5 which just came out yesterday and decompiling the APKs right off the box. It was able to figure out the code path causing the issue and propose a solution. And it ended up being completely right. ™️
Long story short, it seems like there's some sort of server side flag that gets flipped by DirecTV. When either a deep link or channel # tune using ah4c is fired using adb the DirecTV app doesn't register that there was human input. The act of waking it from sleep and sending a command over ADB is what triggers this path. There are actually two flags in the source code. UI_TIMEOUT_OSPREY is the inactivity timer itself, and USE_YOURTV_FOR_STARTUP is what makes it restart playback instead of just dismissing the UI. You need both of them on. They're rolling out slowly to DirecTV users and Osprey boxes probably at random and I must have been the lucky one that got selected. What actually happens is when you sleep the box the app marks playback as terminated for its own "Your TV" startup flow, and nothing in the ADB wake ever clears that mark. So five minutes later the timer fires, it runs that startup flow, and it restores the last channel, which is the channel that was already playing. It's sort of like it thinks you left for Netflix and came back, except nobody ever left. That's what I'm gathering from the analysis I was able to do.
Regardless, sending a heartbeat every three minutes to push back the five minute timer has been working amazingly well and that little bit of json is such an elegant solution for this so much easier than scripting it with ah4c which at some point I'm going to have to do to support that project but I want to wait until it hits more people and see if maybe they pull this feature back.
I just wanted to ask is that feature pretty robust and reliable like do I have to worry about it potentially not firing or like the ADB connection dropping and it's not firing? If I miss a fire then it kind of messes up my stream for a little bit like I get like that blip.
"timed_keep_active_commands": [
{
"run_every_minutes": 3,
"commands": [
"input keyevent 211"
]
}
]
Anyway, sending this random Japanese keystroke every three minutes has solved my problem like completely. It's disappeared, and ADBTuner is the easiest way to implement it by far. So as per usual, amazing work, thank you. And I'm really loving the new dev build and the new UI. It looks so great!!!
I'm glad you found a solution. Yeah. Unfortunately, most of this stuff is quickly getting too tedius to even be worth the effort.
For what it's worth, those commands run every 3 minutes while a tuner is active. It was added to avoid "are you still watching?" prompts. The idea was to find a command that can be sent that would show activity, but not interrupt the active stream in any way. Those commands will fire every X minutes as long as the device is connected, but will stop when the tuner is released. I don't know if that solves your issue entirely.
I've been watching since last night and it solves my issue entirely like it works perfectly for this use case of keeping these streams from reloading after 5 mins. I don't know why DirecTV would implement something so stupid but it works perfectly! Thank you!
This is one of those things that I more or less did for the love of the game lol. I really like Channels and the community here so I wanted to try to find a way to stick it out and find a solution. If this is something that's hitting my boxes eventually it's gonna hit other people too.
If the Osprey is looking for a lower level input, closer to a real, remote button press, would scrcpy register more?
Hi all!
I have been working on setting up a few channels using the HDMI Encoder Native Apps.
I have already successfully added my local CBS channel. Next up: PBS.
If I import the PBS channel list json, using the "Delete" option, will it only delete existing channels from the same input source? (PBS app) I'd hate to have it dump everything I have worked so hard to set up already.
Are you talking about ABDTuner's "Delete" option?

If so, it will delete ALL existing stations, so don't do that!
Or are you talking about OliveTin's "NATIVE_REMOVE" option?
That one only deletes the stations associated with that source app, so is more like a scalpel.
In either case, I'm not sure why you are looking to delete at all? If you loaded up CBS and now you want PBS, it is just an additive function. They don't overlap in any of the settings, so you can just import them in individually without any worries.
My question was triggered by this issue which occurred while importing the PBS json. It skips the main PBS Local channel, identifying it as a duplicate. No such channel currently exists in my ADBT lineup.

I thought that it might be leftover Data from a previous attempt to add PBS. I had hoped to use "Delete" to remove the offending duplicate. Good thing I didn't do that. I'm glad to know of the "Native Remove" Olivetin function. Somehow, I never heard of that action.
Do you have any idea why I received the "skipped duplicate" message, and how I can successfully import both PBS channels in your PBS json?
This is exactly what is happening with PBS. Only the second channel "NHK World" is loaded.
I must have missed this conversation. So much has been changing lately, that it's hard for my aging mind to keep up... my
=

I just pushed a new development build (20260728-1). One change, package name and alternate package name are now considered when determining if a "url" is a duplicate.
This should fix those channel list imports with duplicate values in the URL field.
This was my experience, too. I used the OliveTin "Create Native ADBTuner Channels + CDVR Custom Channels" action, including the "Native Remove" option for PBS, and it loaded both channels from the PBS json.
Thanks for the help!
I believe that OliveTin was using ADBTuner's API to add the channels one at a time which bypassed the validation (that was a bug on my end). The validation is now applied there as well but shouldn't be an issue since the package_name fields are also taken into consideration.
I'll give it a whirl later tonight to see since I got ESPN Unlimited, the true test can begin 
I'm trying to do a net new install to test this against, but keep running into this:
2026-07-28 15:00:12.570 | /app/.venv/bin/python: can't open file '/app/migrate.py': [Errno 2] No such file or directory
2026-07-28 15:00:12.985 | 2026/07/28 15:00:12 Starting proxy server with MPEG-TS null packet support
2026-07-28 15:00:12.985 | 2026/07/28 15:00:12 Loaded routes from file:
2026-07-28 15:00:12.985 |
2026-07-28 15:00:23.437 | 2026/07/28 15:00:23 [PROXY] Proxy error: dial tcp 127.0.0.1:5593: connect: connection refused
2026-07-28 15:00:31.578 | INFO: Started server process [10]
2026-07-28 15:00:31.579 | INFO: Waiting for application startup.
2026-07-28 15:00:31.587 | 2026-07-28 15:00:31.583 - server - ADBTuner 20260728-1
2026-07-28 15:00:31.587 | 2026-07-28 15:00:31.584 - schema_verification - Verifying database schema (/app/.config/config.db).
2026-07-28 15:00:31.810 | 2026-07-28 15:00:31.809 - server - Database schema verification was successful.
2026-07-28 15:00:32.158 | ERROR: Traceback (most recent call last):
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
2026-07-28 15:00:32.158 | self.dialect.do_execute(
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~~~~~~~~^
2026-07-28 15:00:32.158 | cursor, str_statement, effective_parameters, context
2026-07-28 15:00:32.158 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.158 | )
2026-07-28 15:00:32.158 | ^
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
2026-07-28 15:00:32.158 | cursor.execute(statement, parameters)
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.158 | sqlite3.OperationalError: no such table: app_settings
2026-07-28 15:00:32.158 |
2026-07-28 15:00:32.158 | The above exception was the direct cause of the following exception:
2026-07-28 15:00:32.158 |
2026-07-28 15:00:32.158 | Traceback (most recent call last):
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/starlette/routing.py", line 638, in lifespan
2026-07-28 15:00:32.158 | async with self.lifespan_context(app) as maybe_state:
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~~~~~~^^^^^
2026-07-28 15:00:32.158 | File "/usr/local/lib/python3.14/contextlib.py", line 214, in __aenter__
2026-07-28 15:00:32.158 | return await anext(self.gen)
2026-07-28 15:00:32.158 | ^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/fastapi/routing.py", line 216, in merged_lifespan
2026-07-28 15:00:32.158 | async with original_context(app) as maybe_original_state:
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~^^^^^
2026-07-28 15:00:32.158 | File "/usr/local/lib/python3.14/contextlib.py", line 214, in __aenter__
2026-07-28 15:00:32.158 | return await anext(self.gen)
2026-07-28 15:00:32.158 | ^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/fastapi/routing.py", line 216, in merged_lifespan
2026-07-28 15:00:32.158 | async with original_context(app) as maybe_original_state:
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~^^^^^
2026-07-28 15:00:32.158 | File "/usr/local/lib/python3.14/contextlib.py", line 214, in __aenter__
2026-07-28 15:00:32.158 | return await anext(self.gen)
2026-07-28 15:00:32.158 | ^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/fastapi/routing.py", line 216, in merged_lifespan
2026-07-28 15:00:32.158 | async with original_context(app) as maybe_original_state:
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~^^^^^
2026-07-28 15:00:32.158 | File "/usr/local/lib/python3.14/contextlib.py", line 214, in __aenter__
2026-07-28 15:00:32.158 | return await anext(self.gen)
2026-07-28 15:00:32.158 | ^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.158 | File "/app/app/main.py", line 261, in lifespan
2026-07-28 15:00:32.158 | migrate_file_configurations()
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
2026-07-28 15:00:32.158 | File "/app/app/dependencies.py", line 532, in migrate_file_configurations
2026-07-28 15:00:32.158 | .first()
2026-07-28 15:00:32.158 | ~~~~~^^
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/orm/query.py", line 2766, in first
2026-07-28 15:00:32.158 | return self.limit(1)._iter().first() # type: ignore
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~~~~^^
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/orm/query.py", line 2864, in _iter
2026-07-28 15:00:32.158 | result: Union[ScalarResult[_T], Result[_T]] = self.session.execute(
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~~~~~^
2026-07-28 15:00:32.158 | statement,
2026-07-28 15:00:32.158 | ^^^^^^^^^^
2026-07-28 15:00:32.158 | params,
2026-07-28 15:00:32.158 | ^^^^^^^
2026-07-28 15:00:32.158 | execution_options={"_sa_orm_load_options": self.load_options},
2026-07-28 15:00:32.158 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.158 | )
2026-07-28 15:00:32.158 | ^
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/orm/session.py", line 2372, in execute
2026-07-28 15:00:32.158 | return self._execute_internal(
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~~~~~~~^
2026-07-28 15:00:32.158 | statement,
2026-07-28 15:00:32.158 | ^^^^^^^^^^
2026-07-28 15:00:32.158 | ...<4 lines>...
2026-07-28 15:00:32.158 | _add_event=_add_event,
2026-07-28 15:00:32.158 | ^^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.158 | )
2026-07-28 15:00:32.158 | ^
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/orm/session.py", line 2270, in _execute_internal
2026-07-28 15:00:32.158 | result: Result[Any] = compile_state_cls.orm_execute_statement(
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
2026-07-28 15:00:32.158 | self,
2026-07-28 15:00:32.158 | ^^^^^
2026-07-28 15:00:32.158 | ...<4 lines>...
2026-07-28 15:00:32.158 | conn,
2026-07-28 15:00:32.158 | ^^^^^
2026-07-28 15:00:32.158 | )
2026-07-28 15:00:32.158 | ^
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/orm/context.py", line 306, in orm_execute_statement
2026-07-28 15:00:32.158 | result = conn.execute(
2026-07-28 15:00:32.158 | statement, params or {}, execution_options=execution_options
2026-07-28 15:00:32.158 | )
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1421, in execute
2026-07-28 15:00:32.158 | return meth(
2026-07-28 15:00:32.158 | self,
2026-07-28 15:00:32.158 | distilled_parameters,
2026-07-28 15:00:32.158 | execution_options or NO_OPTIONS,
2026-07-28 15:00:32.158 | )
2026-07-28 15:00:32.158 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/sql/elements.py", line 526, in _execute_on_connection
2026-07-28 15:00:32.158 | return connection._execute_clauseelement(
2026-07-28 15:00:32.158 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
2026-07-28 15:00:32.159 | self, distilled_params, execution_options
2026-07-28 15:00:32.159 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.159 | )
2026-07-28 15:00:32.159 | ^
2026-07-28 15:00:32.159 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1643, in _execute_clauseelement
2026-07-28 15:00:32.159 | ret = self._execute_context(
2026-07-28 15:00:32.159 | dialect,
2026-07-28 15:00:32.159 | ...<8 lines>...
2026-07-28 15:00:32.159 | cache_hit=cache_hit,
2026-07-28 15:00:32.159 | )
2026-07-28 15:00:32.159 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
2026-07-28 15:00:32.159 | return self._exec_single_context(
2026-07-28 15:00:32.159 | ~~~~~~~~~~~~~~~~~~~~~~~~~^
2026-07-28 15:00:32.159 | dialect, context, statement, parameters
2026-07-28 15:00:32.159 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.159 | )
2026-07-28 15:00:32.159 | ^
2026-07-28 15:00:32.159 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
2026-07-28 15:00:32.159 | self._handle_dbapi_exception(
2026-07-28 15:00:32.159 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
2026-07-28 15:00:32.159 | e, str_statement, effective_parameters, cursor, context
2026-07-28 15:00:32.159 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.159 | )
2026-07-28 15:00:32.159 | ^
2026-07-28 15:00:32.159 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 2365, in _handle_dbapi_exception
2026-07-28 15:00:32.159 | raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
2026-07-28 15:00:32.159 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
2026-07-28 15:00:32.159 | self.dialect.do_execute(
2026-07-28 15:00:32.159 | ~~~~~~~~~~~~~~~~~~~~~~~^
2026-07-28 15:00:32.159 | cursor, str_statement, effective_parameters, context
2026-07-28 15:00:32.159 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.159 | )
2026-07-28 15:00:32.159 | ^
2026-07-28 15:00:32.159 | File "/app/.venv/lib/python3.14/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
2026-07-28 15:00:32.159 | cursor.execute(statement, parameters)
2026-07-28 15:00:32.159 | ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
2026-07-28 15:00:32.159 | sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: app_settings
2026-07-28 15:00:32.159 | [SQL: SELECT app_settings."key" AS app_settings_key, app_settings.value AS app_settings_value
2026-07-28 15:00:32.159 | FROM app_settings
2026-07-28 15:00:32.159 | WHERE app_settings."key" = ?
2026-07-28 15:00:32.159 | LIMIT ? OFFSET ?]
2026-07-28 15:00:32.159 | [parameters: ('configurations_migrated', 1, 0)]
2026-07-28 15:00:32.159 | (Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-07-28 15:00:32.159 |
2026-07-28 15:00:32.162 | ERROR: Application startup failed. Exiting.
I even did an install against latest, confirmed that was working in general, and then changed to development, but still ended up there. This is not happening against my existing Container, which is functioning just fine:
So I cloned my data volume and pointed my net new install against that. With that, I was able to finally start:
2026-07-28 15:08:28.578 | /app/.venv/bin/python: can't open file '/app/migrate.py': [Errno 2] No such file or directory
2026-07-28 15:08:28.771 | 2026/07/28 15:08:28 Starting proxy server with MPEG-TS null packet support
2026-07-28 15:08:28.784 | 2026/07/28 15:08:28 Loaded routes from file:
2026-07-28 15:08:28.784 | "/proxy/1" -> "http://10.255.1.106:8090/hdmi1"
2026-07-28 15:08:29.849 | 2026/07/28 15:08:29 [PROXY] Proxy error: dial tcp 127.0.0.1:5593: connect: connection refused
2026-07-28 15:08:38.274 | INFO: Started server process [9]
2026-07-28 15:08:38.279 | 2026-07-28 15:08:38.278 - server - ADBTuner 20260728-1
2026-07-28 15:08:38.279 | 2026-07-28 15:08:38.278 - schema_verification - Verifying database schema (/app/.config/config.db).
2026-07-28 15:08:38.279 | INFO: Waiting for application startup.
2026-07-28 15:08:38.284 | 2026-07-28 15:08:38.284 - server - Database schema verification was successful.
2026-07-28 15:08:38.641 | 2026/07/28 15:08:38 Received updated routes, but no changes detected.
2026-07-28 15:08:38.676 | 2026-07-28 15:08:38.676 - tuner_management - Caching installed apps list for all tuners
2026-07-28 15:08:38.706 | 2026-07-28 15:08:38.706 - lib.controller_pool - Pool: creating controller for tuner 1 (10.255.1.168).
2026-07-28 15:08:38.708 | INFO: Application startup complete.
2026-07-28 15:08:38.783 | INFO: Uvicorn running on http://0.0.0.0:5593 (Press CTRL+C to quit)
And after getting going, I could confirm the import worked as expected, at least for all stations:

Thanks for this. I just pushed a new development build (20260728-2) in which you can actually start with a fresh database again. Oops.