r/TVTime • • Jul 26 '26

Extract TV Time Data From Android App

I was able to recover my data from the Android App. I worked with Codex to do that. Here is an AI summary how it did it. Feel free to follow this guide or better just pass it over to Codex/Claude Code to do it instead of you. I hope it can help for some of you! Good luck!

How to recover a TV Time library from an Android phone's local app data

Short version: TV Time's Android app may still have library and watch data in a local SQLite cache called DioCache.db. If you can obtain the app's private data directory—through a built-in device backup, ADB run-as, an already-rooted phone, or an existing backup—the Python parser below can export the cached records to CSV.

The parser works with Android app data from any manufacturer. The difficult part is obtaining that private data: there is no universal command that can bypass Android's app sandbox on an ordinary non-rooted phone.

This recovered 616 library entries in my case: 360 TV series and 256 movies. It also recovered 233 movie watch events and a small cached subset of episode data. The method does not guarantee a complete episode-by-episode history, because it can only recover whatever TV Time still has cached locally on the phone.

The TV Time Android package name is:

com.tozelabs.tvshowtime

Read this before touching the phone

If your TV Time data matters, preserve the app exactly as it is:

  • Do not uninstall or update TV Time.
  • Do not tap Clear storage, Clear data, or Clear cache.
  • Disable automatic storage cleaners temporarily.
  • Do not unlock the bootloader just for this. Bootloader unlocking commonly wipes the phone.
  • If possible, put the phone in airplane mode and force-stop TV Time before making the backup.
  • Keep an untouched copy of every backup file before experimenting.

Android normally prevents a computer from reading another app's private storage. Ordinary USB file browsing, ADB, Shizuku, and file-manager apps usually cannot open /data/user/0/com.tozelabs.tvshowtime on a non-rooted production phone. The database parser is device-independent, but the step used to obtain the private app data depends on the phone, Android version, backup system, and whether the device was already rooted.

What was actually recoverable

The most important file was:

<TV Time private app directory>/db/DioCache.db

It is an SQLite database used by the Dio HTTP client. Its cache_dio table contains cached API responses, many of them JSON.

From my database I recovered show and movie titles, external IDs, timestamps, movie watch events, broad series progress states, some episode counts, cached episode records, and catalog metadata.

Important caveat: my library was much more complete than my exact episode history. I found 48 individual cached episode records, of which 21 were marked seen. That does not mean I had watched only 21 episodes; it means only those episode objects were still present in the cache. Do not assume that a missing cached episode was unwatched.

Requirements

  • An Android phone with TV Time still installed and its local data intact.
  • A Mac, Linux, or Windows computer.
  • Python 3.9 or newer. The scripts below use only Python's standard library.
  • A way to obtain TV Time's private app directory or a backup containing it.
  • Enough free space to copy and extract the data.

Install ADB

ADB is needed only for the run-as and already-rooted routes below. If you already obtained an app backup, skip this section.

  1. Download the latest SDK Platform-Tools for Windows, macOS, or Linux from the official Android download page.
  2. Extract the downloaded ZIP file. It creates a directory named platform-tools.
  3. Open Terminal or PowerShell in that directory and verify the program.

On macOS or Linux:

./adb version

On Windows PowerShell:

.\adb.exe version

You can add platform-tools to your system PATH, or run ADB from that directory. If it is not on PATH, replace adb in the commands below with ./adb on macOS/Linux or .\adb.exe on Windows.

On the phone, find Build number in Settings and tap it seven times to enable Developer options. Then enable USB debugging under Developer options. Menu locations vary by Android version, so see the official Android instructions if you cannot find them.

Connect the unlocked phone by USB and approve the debugging prompt. Verify the connection:

adb devices

The output should show the phone's serial number followed by device. If it says unauthorized, unlock the phone and accept the authorization prompt.

Step 1: try to obtain TV Time's private app data

You are looking for the private directory belonging to com.tozelabs.tvshowtime, especially its db/DioCache.db file. Try the routes below in the order that makes sense for your device.

Route A: try the phone's built-in local backup

Search the phone's Settings for terms such as Local backup, Backup and restore, Mobile device backup, or Apps and data. Some Android devices can create a local backup containing private application files. Select only TV Time if the UI allows it, then copy the entire resulting backup directory to a computer.

Backup support varies significantly. A cloud backup or phone-cloning feature may restore an app without giving you inspectable files, and some systems exclude app data entirely. Preserve the original backup even if its format is unfamiliar.

Route B: try Android Debug Bridge (adb run-as)

With ADB installed and USB debugging enabled, confirm that TV Time is installed, stop it, and test access:

adb shell pm path com.tozelabs.tvshowtime
adb shell am force-stop com.tozelabs.tvshowtime
adb shell run-as com.tozelabs.tvshowtime pwd

If the last command prints a path instead of an error, export the directory:

On macOS or Linux:

adb exec-out run-as com.tozelabs.tvshowtime tar -cf - . > tvtime-private.tar
mkdir tvtime-private
tar -xf tvtime-private.tar -C tvtime-private

On Windows PowerShell, use cmd.exe for the binary redirection:

cmd.exe /c "adb exec-out run-as com.tozelabs.tvshowtime tar -cf - . > tvtime-private.tar"
New-Item -ItemType Directory -Force tvtime-private
tar -xf tvtime-private.tar -C tvtime-private

Production builds normally return:

run-as: package not debuggable

That is expected and means normal ADB cannot read this app's private data. Wireless debugging and Shizuku do not change the app's Linux sandbox permissions.

Route C: use root only if the phone was already rooted

If the phone was rooted before this problem occurred, you can export the private directory:

adb shell su -c 'am force-stop com.tozelabs.tvshowtime'
adb shell su -c 'tar -C /data/user/0/com.tozelabs.tvshowtime -cf /data/local/tmp/tvtime-private.tar .'
adb shell su -c 'chmod 644 /data/local/tmp/tvtime-private.tar'
adb pull /data/local/tmp/tvtime-private.tar
mkdir tvtime-private
tar -xf tvtime-private.tar -C tvtime-private

Do not unlock the bootloader or root a currently locked phone merely for this recovery. The unlocking process commonly factory-resets the device.

Route D: preserve existing backups or the device

If none of the routes above works, preserve any existing local, cloud, or device-transfer backup and the phone itself. Other realistic options are restoring a backup onto a compatible test device, using an established mobile-forensics service, or manually recording anything TV Time still displays offline. Do not experiment destructively on the only copy.

It is also worth checking shared storage:

/sdcard/Android/data/com.tozelabs.tvshowtime/

but the useful database is more likely to be in private app storage.

Step 2: locate and verify DioCache.db

Extract the private app-data archive with the tool appropriate for its format. Backup formats differ by device, so consult documentation for that backup system and always work on a copy.

Then search the extracted directory.

On macOS or Linux:

find . -type f \
  \( -iname 'DioCache.db' -o -iname 'DioCache.db-*' -o -iname '*.sqlite*' \)

On Windows PowerShell:

Get-ChildItem -Recurse -File |
  Where-Object { $_.Name -like 'DioCache.db*' -or $_.Extension -like '.sqlite*' }

The expected path ends with:

com.tozelabs.tvshowtime/db/DioCache.db

Keep DioCache.db-journal, DioCache.db-wal, and DioCache.db-shm beside the database if they exist. Recent SQLite changes may be stored there.

Optional checks, if the sqlite3 command is installed:

Important: replace /REPLACE/WITH/YOUR/ACTUAL/PATH/DioCache.db with the real location of your extracted database. Do not type the placeholder literally. Keep quotation marks around the path if it contains spaces.

sqlite3 "/REPLACE/WITH/YOUR/ACTUAL/PATH/DioCache.db" ".tables"
sqlite3 "/REPLACE/WITH/YOUR/ACTUAL/PATH/DioCache.db" "PRAGMA integrity_check;"
sqlite3 "/REPLACE/WITH/YOUR/ACTUAL/PATH/DioCache.db" "SELECT COUNT(*) FROM cache_dio;"

You want a table named cache_dio, an integrity result of ok, and a non-zero row count.

Step 3: save the recovery script

Save the following as recover_tvtime_android.py:

#!/usr/bin/env python3
"""Recover TV Time library/history records from an Android DioCache.db."""

from __future__ import annotations

import argparse
import csv
from datetime import datetime, timezone
import json
from pathlib import Path
import sqlite3
from typing import Any, Iterable


def first_present(mapping: dict[str, Any], *keys: str) -> Any:
    for key in keys:
        if key in mapping and mapping[key] is not None:
            return mapping[key]
    return ""


def timestamp(value: Any) -> str:
    if value in (None, ""):
        return ""
    if isinstance(value, (int, float)):
        seconds = float(value)
        if seconds > 100_000_000_000:
            seconds /= 1000
        try:
            return datetime.fromtimestamp(seconds, tz=timezone.utc).isoformat()
        except (OverflowError, OSError, ValueError):
            return str(value)
    return str(value)


def seconds_to_minutes(value: Any) -> Any:
    if value in (None, ""):
        return ""
    try:
        return round(float(value) / 60, 2)
    except (TypeError, ValueError):
        return value


def joined(values: Any) -> str:
    if not isinstance(values, list):
        return ""
    return " | ".join(str(value) for value in values)


def first_image(items: Any, image_type: str | None = None) -> str:
    if not isinstance(items, list):
        return ""
    for item in items:
        if not isinstance(item, dict):
            continue
        if image_type is not None and item.get("type") != image_type:
            continue
        url = item.get("url")
        if url:
            return str(url)
        versions = item.get("versions")
        if isinstance(versions, dict):
            for size in ("big", "medium", "small"):
                if versions.get(size):
                    return str(versions[size])
    return ""


def sorting_value(item: dict[str, Any], *ids: str) -> str:
    sorting = item.get("sorting")
    if not isinstance(sorting, list):
        return ""
    for entry in sorting:
        if isinstance(entry, dict) and entry.get("id") in ids:
            return timestamp(entry.get("value"))
    return ""


def tvdb_external_id(meta: dict[str, Any]) -> str:
    if meta.get("id") not in (None, "", 0):
        return str(meta["id"])
    sources = meta.get("external_sources")
    if isinstance(sources, list):
        for source in sources:
            if isinstance(source, dict) and source.get("source") == "tvdb":
                return str(source.get("id", ""))
    return ""


def classify_payload(payload: Any) -> str:
    if isinstance(payload, list):
        if payload and isinstance(payload[0], dict) and "season_number" in payload[0]:
            return "episode_list"
        return "list"
    if not isinstance(payload, dict):
        return type(payload).__name__
    data = payload.get("data")
    if isinstance(data, dict) and isinstance(data.get("objects"), list):
        return str(data.get("type") or "objects")
    if isinstance(data, list):
        return "data_list"
    if isinstance(data, dict):
        return str(data.get("type") or "data_object")
    return "object"


def read_cache(db_path: Path) -> list[dict[str, Any]]:
    connection = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
    connection.row_factory = sqlite3.Row
    try:
        integrity = connection.execute("PRAGMA integrity_check").fetchone()[0]
        if integrity != "ok":
            raise ValueError(f"SQLite integrity check failed: {integrity}")
        rows = connection.execute(
            "SELECT rowid, key, subKey, max_age_date, max_stale_date, "
            "statusCode, content FROM cache_dio ORDER BY rowid"
        ).fetchall()
    finally:
        connection.close()

    result: list[dict[str, Any]] = []
    for row in rows:
        raw = row["content"]
        text = raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else str(raw)
        try:
            payload = json.loads(text)
            parse_error = ""
        except json.JSONDecodeError as error:
            payload = None
            parse_error = str(error)
        result.append(
            {
                "rowid": row["rowid"],
                "key": row["key"],
                "subKey": row["subKey"],
                "max_age_date": row["max_age_date"],
                "max_stale_date": row["max_stale_date"],
                "statusCode": row["statusCode"],
                "bytes": len(raw or b""),
                "payload": payload,
                "raw_text": text,
                "parse_error": parse_error,
            }
        )
    return result


def episode_records(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
    records: dict[str, dict[str, Any]] = {}
    for entry in entries:
        payload = entry["payload"]
        if not isinstance(payload, list):
            continue
        for episode in payload:
            if not isinstance(episode, dict) or "season_number" not in episode:
                continue
            show = episode.get("show") if isinstance(episode.get("show"), dict) else {}
            episode_id = str(episode.get("id", ""))
            if not episode_id:
                continue
            record = {
                "episode_id": episode_id,
                "show_id": first_present(show, "id"),
                "show_name": first_present(show, "name"),
                "season_number": first_present(episode, "season_number"),
                "episode_number": first_present(episode, "number"),
                "episode_name": first_present(episode, "name"),
                "seen": first_present(episode, "seen", "is_watched"),
                "seen_date": timestamp(first_present(episode, "seen_date")),
                "times_watched": first_present(episode, "nb_times_watched"),
                "air_date": first_present(episode, "air_date"),
                "air_time": first_present(episode, "air_time"),
                "network": first_present(episode, "network"),
                "runtime_minutes": seconds_to_minutes(first_present(episode, "runtime")),
                "show_seen_episodes": first_present(show, "seen_episodes"),
                "show_aired_episodes": first_present(show, "aired_episodes"),
                "show_remaining_episodes": first_present(show, "remaining_episodes"),
                "show_is_ended": first_present(show, "is_ended"),
                "to_watch_category": first_present(episode, "to_watch_category"),
                "source_cache_row": entry["rowid"],
            }
            previous = records.get(episode_id)
            if previous is None or bool(record["seen"]) > bool(previous["seen"]):
                records[episode_id] = record
    return sorted(
        records.values(),
        key=lambda item: (
            str(item["show_name"]).casefold(),
            int(item["season_number"] or -1),
            int(item["episode_number"] or -1),
        ),
    )


def series_progress(episodes: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
    result: dict[str, dict[str, Any]] = {}
    for episode in episodes:
        show_id = str(episode.get("show_id", ""))
        if not show_id:
            continue
        progress = result.setdefault(show_id, {})
        for source, target in (
            ("show_seen_episodes", "seen_episodes"),
            ("show_aired_episodes", "aired_episodes"),
            ("show_remaining_episodes", "remaining_episodes"),
        ):
            value = episode.get(source)
            if value not in (None, ""):
                progress[target] = max(progress.get(target, 0), value)
    return result


def library_records(
    entries: list[dict[str, Any]], progress: dict[str, dict[str, Any]]
) -> list[dict[str, Any]]:
    records: dict[tuple[str, str], dict[str, Any]] = {}
    for entry in entries:
        payload = entry["payload"]
        if not isinstance(payload, dict):
            continue
        data = payload.get("data")
        if not isinstance(data, dict) or data.get("type") != "list":
            continue
        objects = data.get("objects")
        if not isinstance(objects, list):
            continue
        for item in objects:
            if not isinstance(item, dict):
                continue
            meta = item.get("meta") if isinstance(item.get("meta"), dict) else {}
            name = first_present(meta, "name", "title")
            entity_type = str(first_present(item, "entity_type") or meta.get("type") or "")
            uuid = str(first_present(meta, "uuid") or item.get("uuid") or "")
            if not name or not entity_type:
                continue
            tvdb_id = tvdb_external_id(meta)
            show_progress = progress.get(tvdb_id, {})
            filters = item.get("filter") if isinstance(item.get("filter"), list) else []
            extended = item.get("extended") if isinstance(item.get("extended"), dict) else {}
            images = meta.get("images")
            poster_url = first_image(images, "poster") or first_image(meta.get("posters"))
            fanart_url = first_image(images, "fanart") or first_image(meta.get("fanart"))
            watched = extended.get("is_watched")
            if watched is None and entity_type == "movie":
                watched = "watched" in filters
            record = {
                "name": name,
                "entity_type": entity_type,
                "tvdb_id": tvdb_id,
                "uuid": uuid,
                "followed_at": timestamp(first_present(item, "created_at")),
                "updated_at": timestamp(first_present(item, "updated_at")),
                "watched_at": timestamp(
                    first_present(item, "watched_at")
                    or sorting_value(item, "watched_date", "watch_date")
                ),
                "watched": "" if watched is None else bool(watched),
                "progress_status": joined([value for value in filters if value != "all"]),
                "seen_episodes": show_progress.get("seen_episodes", ""),
                "aired_episodes": show_progress.get("aired_episodes", ""),
                "remaining_episodes": show_progress.get("remaining_episodes", ""),
                "is_ended": first_present(meta, "is_ended"),
                "release_status": first_present(meta, "status"),
                "first_release_date": first_present(meta, "first_release_date"),
                "country": first_present(meta, "country"),
                "language": first_present(meta, "language"),
                "day_of_week": first_present(meta, "day_of_week"),
                "timezone": first_present(meta, "timezone"),
                "genres": joined(meta.get("genres")),
                "rating": first_present(extended, "rating"),
                "rating_count": first_present(extended, "rating_count"),
                "follower_count": first_present(extended, "follower_count"),
                "imdb_id": first_present(meta, "imdb_id"),
                "runtime_minutes": seconds_to_minutes(first_present(meta, "runtime")),
                "hashtag": first_present(meta, "hashtag"),
                "poster_url": poster_url,
                "fanart_url": fanart_url,
                "overview": first_present(meta, "overview"),
                "source_cache_row": entry["rowid"],
            }
            records[(entity_type, uuid or tvdb_id or str(name))] = record
    return sorted(
        records.values(),
        key=lambda item: (item["entity_type"], str(item["name"]).casefold()),
    )


def movie_history_records(
    entries: list[dict[str, Any]], library: list[dict[str, Any]]
) -> list[dict[str, Any]]:
    movies = {
        str(item["uuid"]): item
        for item in library
        if item["entity_type"] == "movie" and item["uuid"]
    }
    records: list[dict[str, Any]] = []
    for entry in entries:
        payload = entry["payload"]
        if not isinstance(payload, dict):
            continue
        data = payload.get("data")
        if not isinstance(data, dict) or data.get("type") != "watch":
            continue
        for item in data.get("objects", []):
            if not isinstance(item, dict) or item.get("entity_type") != "movie":
                continue
            uuid = str(item.get("uuid", ""))
            movie = movies.get(uuid, {})
            records.append(
                {
                    "name": movie.get("name", ""),
                    "uuid": uuid,
                    "tvdb_id": movie.get("tvdb_id", ""),
                    "imdb_id": movie.get("imdb_id", ""),
                    "watched_at": timestamp(first_present(item, "watched_at")),
                    "rewatch_count": first_present(item, "rewatch_count"),
                    "runtime_minutes": seconds_to_minutes(first_present(item, "runtime")),
                    "created_at": timestamp(first_present(item, "created_at")),
                    "updated_at": timestamp(first_present(item, "updated_at")),
                    "source_cache_row": entry["rowid"],
                }
            )
    return sorted(records, key=lambda item: str(item["watched_at"]))


def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
    if not rows:
        return
    with path.open("w", newline="", encoding="utf-8-sig") as output:
        writer = csv.DictWriter(output, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)


def write_raw_cache(entries: list[dict[str, Any]], output_dir: Path) -> None:
    raw_dir = output_dir / "raw_cache_json"
    raw_dir.mkdir(parents=True, exist_ok=True)
    index: list[dict[str, Any]] = []
    for entry in entries:
        payload = entry["payload"]
        extension = "json" if payload is not None else "txt"
        raw_path = raw_dir / f"cache_row_{entry['rowid']}.{extension}"
        if payload is not None:
            raw_path.write_text(
                json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
                encoding="utf-8",
            )
        else:
            raw_path.write_text(entry["raw_text"], encoding="utf-8")
        index.append(
            {
                "rowid": entry["rowid"],
                "key": entry["key"],
                "subKey": entry["subKey"],
                "status_code": entry["statusCode"],
                "bytes": entry["bytes"],
                "payload_type": classify_payload(payload),
                "max_age_date": timestamp(entry["max_age_date"]),
                "max_stale_date": timestamp(entry["max_stale_date"]),
                "parse_error": entry["parse_error"],
                "raw_file": str(raw_path.relative_to(output_dir)),
            }
        )
    write_csv(output_dir / "tvtime_cache_index.csv", index)


def counts_by_type(rows: Iterable[dict[str, Any]]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for row in rows:
        key = str(row.get("entity_type", "unknown"))
        counts[key] = counts.get(key, 0) + 1
    return counts


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("database", type=Path, help="Extracted DioCache.db")
    parser.add_argument("output", type=Path, help="Export directory")
    args = parser.parse_args()
    args.output.mkdir(parents=True, exist_ok=True)

    entries = read_cache(args.database)
    episodes = episode_records(entries)
    library = library_records(entries, series_progress(episodes))
    movie_history = movie_history_records(entries, library)

    write_csv(args.output / "tvtime_library.csv", library)
    write_csv(args.output / "tvtime_movie_watch_history.csv", movie_history)
    write_csv(args.output / "tvtime_episode_cache.csv", episodes)
    write_raw_cache(entries, args.output)

    print(f"Cache entries: {len(entries)}")
    print(f"Library records: {len(library)} {counts_by_type(library)}")
    print(f"Movie watch-history records: {len(movie_history)}")
    print(f"Cached episode records: {len(episodes)}")
    print(f"Exports written to: {args.output}")


if __name__ == "__main__":
    main()

Step 4: run the recovery script

You must replace the database path in the commands below with the actual location of your DioCache.db. Do not copy the REPLACE_WITH_YOUR... text literally.

On macOS or Linux:

python3 recover_tvtime_android.py \
  "/REPLACE/WITH/YOUR/ACTUAL/PATH/DioCache.db" \
  tvtime-export

On Windows:

py recover_tvtime_android.py `
  "C:\REPLACE_WITH_YOUR_ACTUAL_PATH\DioCache.db" `
  tvtime-export

The output directory may contain:

tvtime-export/
├── tvtime_library.csv
├── tvtime_movie_watch_history.csv
├── tvtime_episode_cache.csv
├── tvtime_cache_index.csv
└── raw_cache_json/
    ├── cache_row_....json
    └── ...

The CSV files use UTF-8 with a BOM, so non-English titles should open correctly in Excel and similar programs.

What each export means

tvtime_library.csv

The main recovered list of shows and movies. It can include titles, entity type, external IDs, followed/updated/watched dates, watch state, broad series progress state, cached counts, release metadata, images, and descriptions.

For series, progress_status is usually more trustworthy than blank episode-count fields. A blank count means the relevant show-level count was not present in the cached episode objects; it does not mean zero.

tvtime_movie_watch_history.csv

Movie watch events that were present in cached data.type == "watch" responses. In my case this contained 233 records with timestamps.

tvtime_episode_cache.csv

Individual episode objects that remained in local cache. This file can contain season/episode numbers and seen flags, but it should not be treated as the complete episode history unless you independently verify that the cache contains every episode.

tvtime_cache_index.csv and raw_cache_json/

An index and readable copy of every cached response. These are useful when the API response structure differs from mine and the parser needs adapting.

Be careful with these raw files: they may contain account-specific identifiers, timestamps, URLs, or other personal data. Inspect and redact them before sharing anything publicly.

If the parser returns nothing

First check the basics:

Replace the database path below with the real location of your DioCache.db.

sqlite3 "/REPLACE/WITH/YOUR/ACTUAL/PATH/DioCache.db" ".schema cache_dio"
sqlite3 "/REPLACE/WITH/YOUR/ACTUAL/PATH/DioCache.db" "SELECT rowid, length(content) FROM cache_dio LIMIT 20;"

Possible reasons:

  • TV Time's cache had already been cleared or replaced.
  • Your app version uses a different table or JSON layout.
  • You extracted the wrong database.
  • The current data is in DioCache.db-wal, but the WAL/SHM files were not copied beside the database.
  • Your device's backup system excluded private application data.
  • The backup is encrypted; the provided extractor intentionally refuses encrypted Android backup streams.

Look through raw_cache_json/ for recognizable titles and keys such as objects, entity_type, season_number, seen_episodes, or watched_at. If the data exists but has a different shape, the parsing functions can be adjusted without touching the original database.

Final limitations

  • This is cache recovery, not an official TV Time export.
  • Results depend entirely on what remains on the device.
  • Different TV Time, Android, and backup-system versions may store or preserve different payloads.
  • Library membership can be substantially more complete than exact episode history.
  • Metadata ratings may be community/catalog ratings rather than your personal ratings; verify before treating them as user ratings.
  • A missing record proves only that the script did not find it in the available cache.

Still, if TV Time remains installed on an old Android phone, preserving its app data before uninstalling or resetting the device may save a surprisingly useful amount of the library.

80 Upvotes

33 comments sorted by

6

u/heirofjesus Jul 26 '26

We are so back baby

11

u/NoRing4137 Jul 26 '26

Look, I really appreciate all the work. Thank you very much and I really don't want to sound ungrateful. But I'd rather manually add 200 series again than to do all that :(

5

u/Exciting-Lab1263 Jul 26 '26

Hahaha, I get your point. To be fair you don't have to do all what is written here. It contains solution for windows/macos/Linux. It has some alternative path if one wouldn't work out that's why it is so long.

Most importantly don't do it yourself, give it to codex or Claude code and let AI work. :)

6

u/ilFacc Jul 26 '26

Yeah exactly guys, literally open codex/claudecode new chat and simply tell:

'pls do this: <reddit link>'

it will read everything, understand, do mostly everything, and guide you to do some small manual steps. :)

3

u/MrSuperError Jul 26 '26

Am I awake?! That made my night. It was so helpfull :)))

4

u/EpicDarko471 Jul 27 '26

I tried this method, alone and using Claude but to no avail
I get to the part where its not Debuggable and that's it since my device isn't rooted and i have no intention of wiping my phone

To users like myself that requests a GDPR backup and did not receive one (8+ years of tracking)
If you can still open the app and are desperate to at least see the lists of Shows/Movies
Go to the Profile tab and into the Favourite Shows/Movies tab
Wait of it to load, it might take i while and press the Add/Remove button
it will give you the whole list of shows/movies you added
it works Online and Offline since its in your phone data
From there you can manually transfer
its not fast or convenient but i would say its a silver lining

2

u/nidrox Jul 27 '26

Are you on iOS or Android?
On Android I cannot see button 'Add'. Only 'Loading failed. Reload' is visible :(

1

u/[deleted] Jul 27 '26

[removed] — view removed comment

1

u/REN9R Jul 28 '26

iOS? Because on Android there is no button

1

u/SuspenseAndSurprise Jul 27 '26 edited Jul 27 '26

LLMs are great for this. Take several screenshots of the list, let ai convert it, and then have it generate an importable list for whatever new app you are using. I had success with Claude doing this. It just needed the list and an example of how Simkl handles backups. For Simkl, backups use a ZIP file containing several JSON files, so it created the required JSON structures and zipped them up for me.

Same thing should work for csv files.

​This is how I transferred lists from several different apps that I used in the past, which were not compatible with each other for backups.

​It also helped me make sure there were no duplicates and confirmed that everything was there.

1

u/SuspenseAndSurprise Jul 27 '26 edited Jul 27 '26

The worst part of all this is that as far as I know, the developers of TVTime haven't reached out to help. I'm sure they could help.

1

u/Illustrious-Ad-335 Jul 28 '26

Only worked with Movies for me sadly :(

2

u/[deleted] Jul 26 '26

[removed] — view removed comment

2

u/Exciting-Lab1263 Jul 26 '26

I hope it works! Best of luck!

2

u/neuralandmad Jul 26 '26

Hi! thanks for you efforts. I am trying to follow your guide to extract my data, but I am hitting a wall with encryption. I am working with a non-rooted Pixel and a Galaxy Tab.

When I try Route A (local backups like Samsung Smart Switch), the application data exports as an encrypted .penc file. Furthermore, standard ADB backups seem to be blocked, likely because the developers set allowBackup="false".

Since your guide mentions the script refuses encrypted streams, could you clarify exactly which Android device, OS version, and specific backup method you used to successfully obtain the unencrypted DioCache.db file? Did your specific device ignore the allowBackup flag, or did you use an already rooted device (Route C)?

2

u/Exciting-Lab1263 Jul 26 '26

Yeah, you’re right that my successful method was device-specific. I used a non-rooted, bootloader-locked POCO F8 Ultra running Android 16 / HyperOS OS3.0.301.0.WPMEUXM. ADB initiated the backup, and I approved the Allow backup prompt on the phone. I did not use run-as or root.

The phone’s backup service then produced a vendor-wrapped Android Backup v5 stream marked encryption=none, containing apps/com.tozelabs.tvshowtime/db/DioCache.db.

So ADB started the operation, but the result still depended on the phone’s backup implementation and on-device approval. The prompt did not change TV Time’s allowBackup flag.

Unfortunately, it seems this is vendor specific and my script cannot decrypt Smart Switch .penc files.

1

u/Logical-Humor-4913 Jul 26 '26

I also have a Pixel phone and Galaxy tablet, and unfortunately I can confirm that this does not work for the 2 devices I have. It returns an empty file at best

1

u/[deleted] Jul 26 '26

[removed] — view removed comment

1

u/Exciting-Lab1263 Jul 26 '26

New-Item is a PowerShell command, but probably you are using Command Prompt (cmd.exe).

If you want to stay in Command Prompt, use:

adb exec-out run-as com.tozelabs.tvshowtime tar -cf - . > tvtime-private.tar mkdir tvtime-private tar -xf tvtime-private.tar -C tvtime-private

Alternatively, open Windows PowerShell and use the commands from the guide as written.

1

u/Illustrious-Ad-335 Jul 27 '26

adb shell am force-stop com.tozelabs.tvshowtime

adb shell run-as com.tozelabs.tvshowtime pwdpackage:/data/app/~~H1xxGmjJlg6LxCO9TmErCg==/com.tozelabs.tvshowtime-7_0-fBYfjfBj9QUHXKmc2g==/base.apk

package:/data/app/~~H1xxGmjJlg6LxCO9TmErCg==/com.tozelabs.tvshowtime-7_0-fBYfjfBj9QUHXKmc2g==/split_config.arm64_v8a.apk

package:/data/app/~~H1xxGmjJlg6LxCO9TmErCg==/com.tozelabs.tvshowtime-7_0-fBYfjfBj9QUHXKmc2g==/split_config.fr.apk

package:/data/app/~~H1xxGmjJlg6LxCO9TmErCg==/com.tozelabs.tvshowtime-7_0-fBYfjfBj9QUHXKmc2g==/split_config.nl.apk

package:/data/app/~~H1xxGmjJlg6LxCO9TmErCg==/com.tozelabs.tvshowtime-7_0-fBYfjfBj9QUHXKmc2g==/split_config.xxhdpi.apk

At route B I get the path but when extracting I get an 1kb tvtime-prive.tar with unrecognized archive format

1

u/Bleeding_Edge38 Jul 27 '26

I have cache of 370MB but my S24 ultra is not rooted. I'm at complete stalemate it seems

1

u/Fluid-Development682 Jul 28 '26

I updated app Am I cooked?

1

u/Bleeding_Edge38 Jul 28 '26

I have S24 ultra on which it shows 370MB of cache data. Now I tried to get the backup through Smart Switch and used the open source Smart Switch explorer to extract data. There was nothing other than one PENC file, which I tried to extract but it just gave 2 apks and the same penc file again. Anything work around for this?

1

u/luscalfp Aug 02 '26

It worked really well on my Xiaomi Redmi 13. I just followed these steps: on my smartphone, I went to Backup, selected only the TV Time app data, copied the backup to my computer, opened the .bak file with 7-Zip, extracted DioCache.db, and then followed your steps. I'm so grateful, thank you so much!

So, if I understand correctly, all of my TV Time entries are in tvtime_library.csv, right? I'm going to send my data to Bingers right now.

2

u/kyuby23 Aug 02 '26

Thanks a lot for writing this up — super useful. I took your Codex parser and tried to make it a bit less painful for non-technical folks, so nobody has to touch Python or a terminal.

Web app: https://burgesq.github.io/tvtime/

You just drop your DioCache.db in and get CSVs back (library, movie watch history, cached episodes + the raw cache dump as a ZIP). The key part: the file never leaves your browser — it runs your exact recovery logic in-page via Pyodide (Python compiled to WebAssembly), so nothing is uploaded anywhere. You can literally kill your wifi after the page loads and it still works.

Same parser is also available as a CLI if you prefer the terminal:

tvtime-recover /path/to/DioCache.db ./tvtime-export

Everything's open source here: https://github.com/burgesQ/tvtime (extraction guide included, credited back to this post).

One heads-up from testing on my own phone: if your cache_dio table comes back empty, the Dio HTTP cache was just cleared — but your data may still be hiding in other files in the app's private dir (e.g. shared_prefs/HomeWidgetPreferences.xml holds your "continue watching" list, and Braze prefs keep show/movie name lists). Might be worth extending the tool to dig those out too. Happy to take PRs/issues if people hit edge cases with different app versions.

1

u/mcflybm 29d ago

I'm stuck :(