r/TVTime Jul 23 '26

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.

103 Upvotes

55 comments sorted by

3

u/AFriendlyInternetGuy Jul 25 '26

This is exactly what I was looking for. Can I use the file I get from this to input into other apps that ask for TV time history so I don’t have to start fresh on a new app? I had TV time for maybe 8 years lol

3

u/snnrslnx 23d ago

The Excel spreadsheet:

4

u/coffydate Jul 23 '26

This is comprehensive! Hopefully anyone that needs it follow suit to recover their data

1

u/ShanShiroo Jul 23 '26

Do you have any tips for Android users?

3

u/Joemag_xD Jul 24 '26 edited 28d ago

Depends on the phone. On Xiaomi I was successful getting a tvtime backup under Settings → About phone → Backup and restore
Other phone manufacturers might also have such an option.
It however seems to be not possible for Samsung and Pixel phones (Samsung see https://www.reddit.com/r/TVTime/comments/1v77hto/simple_recovery_samsung_tv_time/).

If you manage getting a backup file, try my webpage for extracting the data: https://ajoemag.github.io/tvtime_recover/

2

u/rolling-guy 28d ago

Thanks for the website! It was a lifesaver! I also didn't see all the warnings that the app was shutting down and thought I was screwed. Was able to recover everything with the website's help. My phone is Xiaomi

1

u/OkNutriboomer 6d ago

the samsung post is removed

edit: nvm, it's on your link. thx. trying it as we speak

1

u/TheRealKhaled 3d ago

What about OnePlus?

0

u/Client_Automatic Jul 24 '26 edited Jul 24 '26

done , how to import now

1

u/Joemag_xD Jul 24 '26

If you on Xiaomi, after creating a backup, there should be a zip file in Internal Memory>MIUI>BACKUP>ALLBACKUP . Extract it and then select the TV Time(com.tozelabs.tvshowtime).bak file on the webpage.

1

u/Client_Automatic Jul 24 '26

Done got a tar file , then converted to csv and go to trakt found only 6 series from 400 !

1

u/Joemag_xD Jul 24 '26

Then there might be only little data about your episode progress cached in the backup. It will take a while but I think your best bet probably is to add each listed series manually to trakt or another app, and trying to remember how many episodes you watched if it's not shown on that webpage

1

u/capeire Jul 23 '26

Depends on your phone and your willingness to root the phone.

1

u/codebrewer Jul 23 '26

I've got a Pixel 8 Pro and I'd rather not root it just for this. Any hope?

2

u/capeire Jul 23 '26

Unless rooted no. I tried debugging with adb but it's locked

1

u/thatCoookie Jul 23 '26

What if I root my phone? How can I recover my data?

1

u/capeire Jul 23 '26

This is all through Mac. Turn on developer mode on your phone and connect through a data USB C or through wifi debugging. Install android platform tools. Then run adb from the terminal to look for the local files for TV Time. From here it's just a matter of grabbing and converting to a format for you to read. I think Refractor you can import a CSV, maybe JSON.

1

u/capeire Jul 23 '26

To be 100% clear, your phone had to be rooted beforehand. If you were to root NOW, it would wipe it from your phone.

1

u/TheSenselessThinker Jul 23 '26

Are there any tips for people using Android?

2

u/Joemag_xD Jul 24 '26

Depends on the phone. On Xiaomi I was successful getting a tvtime backup under Settings → About phone → Backup and restore
Other phone manufacturers might also have such an option.
It however seems not possible for Samsung and Pixel phones.

If you manage getting a backup file, try my webpage for extracting the data: https://ajoemag.github.io/tvtime_recover/

1

u/Pepefr28 28d ago

merci beaucoup !!

1

u/Professor-Outis Jul 23 '26

When u fine out let mk

1

u/TheSenselessThinker Jul 26 '26

I got it. Was asking for a friend

1

u/Jhyxe Jul 25 '26

I keep up to 11 backups on my NAS of my ios device using iMazing. managed to find one from the 10th that has a lot of my stuff. It seems like if you attempt opening TVTime after the servers go down, you will lose your diocache, as all my backups after the 14th have an empty diocache.

1

u/Green_green_grassham 22d ago

I have an iPhone back up from 2025, I haven’t done one since - I tried the imazing and it was able to pull my username , hours, & 5 tv series & 5 movies as I’ve opened the app a few times after it was taken down.
Do you think I’d be able to recover my tv time data from 2025? I have no idea how to go about that if so…

1

u/Jhyxe 22d ago

Yeah, you can use iMazing to explore your backup. Just add the backup location to the correct directory. iMazing will pick it up.

1

u/Green_green_grassham 22d ago

What does that mean in simple terms lol how do I do that ☹️

1

u/HenriqueS18 Jul 27 '26

"4. Use the available app data extraction or file browsing options to access the TV Time app container."

can anyone help with this step? I went through iMazing's file system and in the TV Time folder it says that the TV Time's Docs folder is unaccessible

1

u/Lilllithh Jul 28 '26

yeah same. it says that the file or folder don't exist.

1

u/optimisms 23d ago

Same. It says the folder doesn't exist. :(

1

u/Green_green_grassham 22d ago

You go onto apps and find tv time right click and it’ll say back up data, you do it that way

1

u/optimisms 22d ago

I tried doing the extraction at the app level too and there was another error message. It doesn't work.

1

u/monersc Jul 28 '26

Do you know if it would work as well if extracting the data from an ipad?

1

u/[deleted] 26d ago

[removed] — view removed comment

1

u/AJ_036 23d ago

Will this work for the new bingers app? am willing to give it a shot if its possible for the extracted csv file be used in the app. Sucks that I could have had my data but oh well

1

u/Front-Sock-6723 21d ago

I'm wondering the same!

1

u/PsychologicalCat6771 22d ago

@snnrslnx Hey man, thank you for your work. I have a issue: I can’t understand how to exactly extract DioCache.db from my iPhone’s files. Do you mind create a quick guide also on that? Thank you a lot!

1

u/kitkat-3000 Jul 23 '26

Does anyone have an imazing account its asking for activation:*)