u/snnrslnx

▲ 45 r/TVTime

How to recover your TV Time data using iMazing and Python (Free Script)

If you have been looking for a way to recover your TV Time library, followed shows, watch history, and cached metadata from the iOS app, I wanted to share how I managed to recover and export my data into a CSV file.

This method worked for me after the TV Time service was shut down and I could no longer access my data normally. I used iMazing to extract the local TV Time app data from my iPhone, then analyzed the extracted files with a small Python script.

I am sharing this in case it can help other people who lost access to their TV Time data.

OVERVIEW

TV Time does not provide a simple built-in way to export all of your cached library information and watch-related metadata.

After extracting the local app data with iMazing, I found that the TV Time app had stored a significant amount of information locally inside its Documents directory.

The most useful file I found was:

DioCache.db

This is an SQLite database used by the app's HTTP client, Dio. It contains cached API responses and other locally stored information.

Depending on the data that was still available in the local cache, I was able to recover information such as:

Show and movie names
Show IDs
Watched episode counts
Total episode counts
Follow information
Watch status
Show status
Country
Day of the week
Ratings
Follower counts
Hashtags
Poster URLs
Fanart URLs
Various timestamps and dates

I also found several binary files beginning with the bplist00 header. These are Apple Binary Property List files and may contain additional cached application data.

STEP 1: EXTRACTING THE TV TIME DATA WITH IMAZING

  1. Connect your iPhone or iPad to your computer.
  2. Open iMazing.
  3. Find the TV Time app in the Apps section.
  4. Use the available app data extraction or file browsing options to access the TV Time app container.
  5. Export or copy the Documents folder to your computer.

In my case, the extracted Documents folder contained DioCache.db along with many other files with names similar to random hexadecimal hashes.

I recommend keeping the original extracted data untouched and making a backup copy before running any scripts.

STEP 2: EXAMINING THE DATA

The main database I worked with was DioCache.db.

The database contains a table called cache_dio. The content column contains cached data returned by the app's network requests.

Many of these entries are JSON data stored as text or binary data.

The Python script below scans the database, attempts to decode the cached JSON responses, looks for show and movie information, and also checks the other files in the Documents directory for Binary Property List data.

The script then combines the recovered records, removes duplicates, and exports the result to a CSV file.

STEP 3: RUNNING THE PYTHON SCRIPT

The script uses only Python's standard library. No third-party packages are required.

Save the script below as:

parse_tvtime.py

Place it in the same directory as your Documents folder.

The directory structure should look like this:

parse_tvtime.py
Documents
DioCache.db
other extracted files

Then run:

python parse_tvtime.py

If everything works correctly, the script will create:

tvtime_export.csv

The CSV file is saved using UTF-8-SIG encoding, which makes it easier to open correctly in Microsoft Excel when the data contains characters from different languages.

PYTHON SCRIPT

import os
import re
import json
import sqlite3
import plistlib
import csv
from datetime import datetime BASE_DIR = os.path.dirname(os.path.abspath(file))
DOCS_DIR = os.path.join(BASE_DIR, "Documents")
DB_PATH = os.path.join(DOCS_DIR, "DioCache.db")
OUTPUT_CSV = os.path.join(BASE_DIR, "tvtime_export.csv")
 
def format_timestamp(val):
"""Convert Unix timestamps or ISO 8601 dates to YYYY-MM-DD HH:MM:SS."""

if not val:
    return ""

if isinstance(val, (int, float)) or (
    isinstance(val, str) and val.isdigit()
):
    try:
        ts = float(val)

        if ts > 1e11:
            ts /= 1000.0

        return datetime.fromtimestamp(ts).strftime(
            "%Y-%m-%d %H:%M:%S"
        )

    except Exception:
        return str(val)

if isinstance(val, str):
    val_clean = val.replace("Z", "+00:00")

    try:
        dt = datetime.fromisoformat(val_clean)
        return dt.strftime("%Y-%m-%d %H:%M:%S")

    except Exception:
        if "T" in val:
            return val.split("T")[0]

        return val

return str(val)

def extract_image_url(images, img_type="poster"):
if not images or not isinstance(images, list):
return ""

for img in images:
    if isinstance(img, dict) and img.get("type") == img_type:
        return (
            img.get("url")
            or img.get("versions", {}).get("big", "")
        )

return ""

def process_item_dict(d, source_name="DioCache.db"):
if not isinstance(d, dict):
return None

item_data = (
    d.get("meta")
    if isinstance(d.get("meta"), dict)
    else d
)

name = item_data.get("name") or item_data.get("title")

if not name or not isinstance(name, str):
    return None

if len(name.strip()) == 0:
    return None

entity_type = (
    d.get("entity_type")
    or item_data.get("type")
    or "series"
)

if entity_type not in ["series", "movie", "show"]:
    entity_type = "series"

item_id = (
    item_data.get("id")
    or item_data.get("uuid")
    or ""
)

status = item_data.get("status") or ""
country = item_data.get("country") or ""
day_of_week = item_data.get("day_of_week") or ""

extended = (
    item_data.get("extended")
    or d.get("extended")
    or {}
)

if isinstance(extended, dict):
    rating = extended.get("rating") or ""
    follower_count = (
        extended.get("follower_count") or ""
    )
else:
    rating = ""
    follower_count = ""

progress_status = ""

filters = (
    item_data.get("filters")
    or d.get("filter")
    or []
)

if isinstance(filters, list):
    for f in filters:
        if isinstance(f, dict):
            if f.get("id") == "progress":
                vals = f.get("values", [])

                if vals:
                    progress_status = ", ".join(
                        str(v) for v in vals
                    )

        elif isinstance(f, str):
            progress_status += f + " "

progress_status = progress_status.strip()

watched_episodes = (
    item_data.get("watched_episode_count")
    or item_data.get("watched_count")
    or 0
)

total_episodes = (
    item_data.get("aired_episode_count")
    or item_data.get("episode_count")
    or 0
)

is_followed = (
    d.get("is_followed")
    if "is_followed" in d
    else item_data.get("is_followed", True)
)

created_at = (
    d.get("created_at")
    or item_data.get("created_at")
    or ""
)

updated_at = (
    d.get("updated_at")
    or item_data.get("updated_at")
    or ""
)

follow_date = format_timestamp(created_at)

last_watched = None

sorting = (
    item_data.get("sorting")
    or d.get("sorting")
    or []
)

if isinstance(sorting, list):
    for s in sorting:
        if isinstance(s, dict):
            sid = s.get("id")

            if sid in [
                "watch_date",
                "last_watched",
                "follow_date"
            ]:
                val = s.get("value")

                if val and not last_watched:
                    last_watched = val

last_watched_date = format_timestamp(
    last_watched or updated_at
)

hashtag = item_data.get("hashtag") or ""

images = item_data.get("images") or []

poster_data = item_data.get("poster") or {}
background_data = item_data.get("background") or {}

poster_url = (
    extract_image_url(images, "poster")
    or (
        poster_data.get("url", "")
        if isinstance(poster_data, dict)
        else ""
    )
)

fanart_url = (
    extract_image_url(images, "fanart")
    or (
        background_data.get("url", "")
        if isinstance(background_data, dict)
        else ""
    )
)

return {
    "name": name.strip(),
    "type": entity_type,
    "id": str(item_id),
    "status": str(status),
    "progress_status": progress_status,
    "watched_episodes": watched_episodes,
    "total_episodes": total_episodes,
    "is_followed": bool(is_followed),
    "country": country,
    "day_of_week": day_of_week,
    "rating": rating,
    "follower_count": follower_count,
    "follow_date": follow_date,
    "last_watched_date": last_watched_date,
    "hashtag": hashtag,
    "poster_url": poster_url,
    "fanart_url": fanart_url,
    "data_source": source_name
}

def parse_sqlite_db(db_path):
items = []

if not os.path.exists(db_path):
    return items

conn = sqlite3.connect(db_path)
cursor = conn.cursor()

try:
    cursor.execute(
        "SELECT content FROM cache_dio"
    )

    rows = cursor.fetchall()

    for row in rows:
        raw_content = row[0]

        if not raw_content:
            continue

        if isinstance(raw_content, bytes):
            text_content = raw_content.decode(
                "utf-8",
                errors="ignore"
            )
        else:
            text_content = str(raw_content)

        try:
            data_json = json.loads(text_content)

            if isinstance(data_json, list):
                for elem in data_json:
                    res = process_item_dict(
                        elem,
                        "DioCache.db"
                    )

                    if res:
                        items.append(res)

            elif isinstance(data_json, dict):

                for key in [
                    "series",
                    "shows",
                    "movies",
                    "data",
                    "items"
                ]:
                    if (
                        key in data_json
                        and isinstance(
                            data_json[key],
                            list
                        )
                    ):
                        for elem in data_json[key]:
                            res = process_item_dict(
                                elem,
                                "DioCache.db"
                            )

                            if res:
                                items.append(res)

                res = process_item_dict(
                    data_json,
                    "DioCache.db"
                )

                if res:
                    items.append(res)

        except Exception:

            matches = re.findall(
                r'(\{"id":\d+,"name":".*?\})',
                text_content
            )

            for m in matches:
                try:
                    parsed = json.loads(m)

                    res = process_item_dict(
                        parsed,
                        "DioCache.db"
                    )

                    if res:
                        items.append(res)

                except Exception:
                    pass

finally:
    conn.close()

return items

def parse_bplist_files(docs_dir):
items = []

if not os.path.exists(docs_dir):
    return items

files = [
    f
    for f in os.listdir(docs_dir)
    if f != "DioCache.db"
]

for filename in files:

    file_path = os.path.join(
        docs_dir,
        filename
    )

    if not os.path.isfile(file_path):
        continue

    try:
        with open(file_path, "rb") as f:

            header = f.read(8)

            if header.startswith(b"bplist"):

                f.seek(0)

                plist_data = plistlib.load(f)

                if isinstance(
                    plist_data,
                    dict
                ):

                    res = process_item_dict(
                        plist_data,
                        f"bplist ({filename[:8]})"
                    )

                    if res:
                        items.append(res)

    except Exception:
        pass

return items

def merge_and_deduplicate(items):

merged = {}

for item in items:

    key = (
        item["name"].lower(),
        item["id"]
    )

    if key not in merged:
        merged[key] = item

    else:

        existing = merged[key]

        for field in [
            "status",
            "progress_status",
            "country",
            "day_of_week",
            "rating",
            "follower_count",
            "hashtag",
            "poster_url",
            "fanart_url"
        ]:

            if (
                not existing.get(field)
                and item.get(field)
            ):
                existing[field] = item[field]

        if (
            item.get("watched_episodes", 0)
            > existing.get(
                "watched_episodes",
                0
            )
        ):
            existing["watched_episodes"] = (
                item["watched_episodes"]
            )

        if (
            item.get("total_episodes", 0)
            > existing.get(
                "total_episodes",
                0
            )
        ):
            existing["total_episodes"] = (
                item["total_episodes"]
            )

        if (
            item.get("last_watched_date")
            and item["last_watched_date"]
            > existing.get(
                "last_watched_date",
                ""
            )
        ):
            existing["last_watched_date"] = (
                item["last_watched_date"]
            )

result = list(merged.values())

result.sort(
    key=lambda x: x["name"].lower()
)

return result

def export_to_csv(items, output_path):

fieldnames = [
    "name",
    "type",
    "id",
    "status",
    "progress_status",
    "watched_episodes",
    "total_episodes",
    "is_followed",
    "country",
    "day_of_week",
    "rating",
    "follower_count",
    "follow_date",
    "last_watched_date",
    "hashtag",
    "poster_url",
    "fanart_url",
    "data_source"
]

with open(
    output_path,
    "w",
    newline="",
    encoding="utf-8-sig"
) as csvfile:

    writer = csv.DictWriter(
        csvfile,
        fieldnames=fieldnames
    )

    writer.writeheader()

    for item in items:
        writer.writerow(item)

def main():

sqlite_items = parse_sqlite_db(
    DB_PATH
)

bplist_items = parse_bplist_files(
    DOCS_DIR
)

all_items = (
    sqlite_items
    + bplist_items
)

unique_items = merge_and_deduplicate(
    all_items
)

export_to_csv(
    unique_items,
    OUTPUT_CSV
)

print(
    f"Exported {len(unique_items)} "
    "unique shows/movies to "
    "tvtime_export.csv"
)

if name == "main":
main()

RESULT

The final CSV file contains the recovered records that the script was able to identify from the extracted local data.

Depending on what data is still present in your TV Time app cache, the exported file may contain information such as:

Show or movie title
TV Time or TVDB related ID
Watched episode count
Total episode count
Follow status
Watch progress
Show status
Follow date
Last watched date
Country
Day of the week
Rating
Follower count
Hashtags
Poster URL
Fanart URL
Source of the recovered record

IMPORTANT NOTES

This method does not guarantee that every piece of TV Time data can be recovered.

The results depend entirely on what information is still stored locally on the device. If the app cache was cleared, the device was reset, or the relevant data was never stored locally, the script may not be able to recover it.

Also, the database structure and cached API responses may differ between TV Time app versions. The script is therefore intended as a starting point for analyzing an extracted TV Time app container rather than a guaranteed universal recovery tool.

In my case, iMazing allowed me to access the local app data that was still available on my device, and analyzing the SQLite database and other cached files helped me recover a significant amount of my TV Time library information.

If you still have an old iPhone or iPad that had TV Time installed, it may be worth checking whether the app's local data is still available before deleting the app or resetting the device.

I hope this helps anyone trying to recover their TV Time library or watch history.

reddit.com
u/snnrslnx — 1 day ago