Skip to content

Makes episode sync work#44

Open
mhrivnak wants to merge 4 commits intooxtyped:mainfrom
mhrivnak:episodes
Open

Makes episode sync work#44
mhrivnak wants to merge 4 commits intooxtyped:mainfrom
mhrivnak:episodes

Conversation

@mhrivnak
Copy link
Copy Markdown

I've tested this with Antennapod, and episode sync is working perfectly.

This merges #18 and completes that work. Thanks @TheBlusky !

fixes #39

TheBlusky and others added 4 commits December 6, 2023 19:18
The timestamp is being stored in the DB as an integer, but in a
field of type varchar(255). So the DB cannot be used to sort of filter,
because it would sort the integers alphabetically.

The workaround is to load all of the results into memory and then
throw away any that don't meet the optional `since=` filter.

fixes oxtyped#39
@mhrivnak
Copy link
Copy Markdown
Author

I couldn't figure out how to run the tests. They seem to require a pre-existing database, but I'm not sure what's expected there, and I didn't see anything in the repo that would generate it. I did manually test this with antennapod, but I don't know if the automated tests would pass after these changes.

@mhrivnak
Copy link
Copy Markdown
Author

mhrivnak commented Dec 14, 2024

If anyone else wants to try this in the meantime, you can use container image quay.io/mhrivnak/gpodder2go-container:mhrivnak-fork

Just take note that I made a new `Containerfile' which has the process running as non-root (user 1001), so be mindful of permissions when mounting storage there.

Update: I forgot to mention that I also put the binary in /usr/bin/ instead of just dropping in at /. So when you run commands to create an account for example, you can just name the command like any other without a / prefix. For example:

podman exec -it $CONTAINER_NAME gpodder2go accounts create ...

@update-freak
Copy link
Copy Markdown

so when I would like to try it I have to change the image to quay.io/mhrivnak/gpodder2go-container:mhrivnak-fork or additional things are needed?

@mhrivnak
Copy link
Copy Markdown
Author

Here's what I did to run it locally on Fedora and use a directory on my system to store the data:

mkdir data
chmod g+w data
podman run -d --name gpodder2go -p 3005:3005 -v ./data/:/data:Z quay.io/mhrivnak/gpodder2go-container:mhrivnak-fork

If you're still using docker, the same arguments should work. And you might not need the :Z if you're running on a system without selinux.

@update-freak
Copy link
Copy Markdown

When I use the following docker compose file in Portainer and start with an empty gpodder2go-folder I got the following error message:

services:
gpodder2go:
image: quay.io/mhrivnak/gpodder2go-container:mhrivnak-fork
container_name: gpodder2go
ports:
- 3005:3005
environment:
- NO_AUTH=false
- VERIFIER_SECRET_KEY=""
volumes:
- /volume1/docker/gpodder2go:/data
restart: unless-stopped

No database found, intializing gpodder2go ...
2024/12/14 16:39:03 unable to open database file: out of memory (14)

@mhrivnak
Copy link
Copy Markdown
Author

Yeah, that error message is misleading, and it's a bug in the sqlite3 driver. The problem is just that the process can't write to the /data/ directory.

The process running in the container will run as UID 1001, instead of running as root. That's a good thing. We just need to make sure that user 1001 can write to the data directory. Sorry, I haven't used docker in many years, so I can't test with it. One of the main reasons to use podman is that it makes it easier to run things as non-root.

Can you run sudo chmod g+w /volume1/docker/gpodder2go on the host system and see if that fixes it? The user inside the container should still have its group set to 0 aka root, so giving write permissions to the root group should work.

@update-freak
Copy link
Copy Markdown

Thanks for the explanation. I fixed it via giving read+write access to everyone in the Synoloy DSM GUI for this folder.
Via the command in SSH it did not work via sudo chmod g+w /volume1/docker/gpodder2go

Then I tried to add a user via:
docker exec -it DOCKER-ID /gpodder2go accounts create Felix -e felix@beispiel.de -n Felix -p PASSWORT

But I got this error:
OCI runtime exec failed: exec failed: unable to start container process: exec: "accounts": executable file not found in $PATH: unknown

@mhrivnak
Copy link
Copy Markdown
Author

Ah yes, sorry, I forgot to mention one other change! I put the binary in the standard location for binaries instead of at /.

Just remove the / in front of gpodder2go in your command.

docker exec -it DOCKER-ID gpodder2go accounts create ...

@update-freak
Copy link
Copy Markdown

Yes, without / it works. Thanks a lot! :)

@update-freak
Copy link
Copy Markdown

After synchronisation I recognised that the sync is failed in AntennaPod.
Then I checked the logs in Portainer I found this line:
missing cookie, have you logged in yet: &errors.errorString{s:"http: named cookie not present"}

I have attached my podcast subscriptions and also the log from portainer.
antennapod-feeds-2024-12-15.opml.txt
gpodder2go_logs.txt

@mhrivnak
Copy link
Copy Markdown
Author

I suggest opening a new issue on this repo for that, as it's likely unrelated to the episode API.

The auth check happens early in the code flow, before any of the API handlers get invoked. Reading the code, I don't see how the no cookie message could happen unless the request truly did not include the required auth cookie. Perhaps it's even a bug in antennapod, that it sent a bad request?

@update-freak
Copy link
Copy Markdown

Now I started with a fresh installation of AntennaPod and added my subscriptions and checked it with a second device.
It synchronizes now without any problems the subscriptions and also the listened episodes.

@thekoma
Copy link
Copy Markdown

thekoma commented Feb 24, 2025

This PR is pretty interesting!

@clach04
Copy link
Copy Markdown

clach04 commented Mar 28, 2026

@mhrivnak thanks for this, the doc change you made was the game-changer for me (manual group setup). I hope to have time to play with your fix(es), as the sole reason I want a sync server is episode progress. Thanks for working on this and sharing 🙏

I just posted a PR to your fork with a minor doc cleanup, so your new docs are readable in a browser https://github.com/clach04/gpodder2go/tree/patch-1#antennapod

@thekoma
Copy link
Copy Markdown

thekoma commented Mar 28, 2026

I am working on a complete rewrite of gpodder called rpodder https://github.com/thekoma/rpodder . It's heavily vibe coded, nonetheless does what I need and some fancy thing more. @mhrivnak PR are welcome.

@clach04
Copy link
Copy Markdown

clach04 commented Mar 28, 2026

Thanks for the heads up @thekoma , that looks neat! It's about triple the size of this and has more features than I need right now, so I'll see how my usage goes.

@mhrivnak I put together a python script for step 5 in your notes about setting up a group - its a bit of a hammer as it marks all devices for sync

#!/usr/bin/env python
# -*- coding: us-ascii -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
#
"""Mark ALL devices known to username, as syncronized.

gpodder2go notes

1. Need fixed version with Episode sync support - https://github.com/oxtyped/gpodder2go/pull/44
2. can't use a device with no subscriptions, see readme in https://github.com/clach04/gpodder2go for specific steps. This script handles step #5

"""

import argparse
import json
import os
import sys

import requests  # pip install requests


def doit(args, timeout=10):
    """arguments:
        args.username
        args.password
        args.url
    """

    url_root = args.url
    while url_root.endswith('/'):
        print(url_root)
        url_root = url_root[:-1]
    print(url_root)

    # https://gpoddernet.readthedocs.io/en/latest/api/reference/auth.html
    url_login = url_root + '/api/2/auth/%s/login.json' % (args.username,)

    # https://gpoddernet.readthedocs.io/en/latest/api/reference/devices.html
    # https://gpoddernet.readthedocs.io/en/latest/api/reference/sync.html
    url_devices = url_root + '/api/2/devices/%s.json' % (args.username,)
    url_sync_devices = url_root + '/api/2/sync-devices/%s.json' % (args.username,)

    # Using a session object allows for (in-memory) cookie persistence
    session = requests.Session()

    try:
        # use HTTP Basic Auth and login
        auth = (args.username, args.password) if args.username and args.password else None
        response = session.post(url_login, auth=auth, timeout=timeout)
        response.raise_for_status()
        print(f"--- POST Status Code: {response.status_code} ---")
        print(response.text)

        all_devices_ids = []
        response = session.get(url_devices, timeout=timeout)
        response.raise_for_status()
        print(f"--- GET Status Code: {response.status_code} ---")
        print(response.text)
        print(response.json())
        print(json.dumps(response.json(), indent=4))
        print(dir(response))
        for device in response.json():
            all_devices_ids.append(device["id"])

        response = session.get(url_sync_devices, timeout=timeout)
        response.raise_for_status()
        print(f"--- GET Status Code: {response.status_code} ---")
        print(response.text)

        print(all_devices_ids)
        sync_dict = {
            "synchronize": [
                all_devices_ids  # Yes, list in a list (array in an array)
            ],
            #"stop-synchronize": [],
            #"stop-synchronize": None,
        }
        response = session.post(url_sync_devices, auth=auth, timeout=timeout, json=sync_dict)
        response.raise_for_status()
        print(f"--- POST Status Code: {response.status_code} ---")
        print(response.text)

        response = session.get(url_sync_devices, timeout=timeout)
        response.raise_for_status()
        print(f"--- GET Status Code: {response.status_code} ---")
        print(response.text)

    except requests.exceptions.HTTPError as errh:
        print(f"HTTP Error: {errh}")
    except requests.exceptions.ConnectionError as errc:
        print(f"Error Connecting: {errc}")
    except requests.exceptions.Timeout as errt:
        print(f"Timeout Error: {errt}")
    except requests.exceptions.RequestException as err:
        print(f"An unexpected error occurred: {err}")
    finally:
        # Close the session to free up resources
        session.close()

def main(argv=None):
    if argv is None:
        argv = sys.argv

    # FIXME argv
    parser = argparse.ArgumentParser(description="Root URL for gpodder compatible server, omit /api/2/...")
    parser.add_argument("url", help="The target URL (e.g., http://localhost:3005)")
    parser.add_argument("-u", "--username", help="Username for authentication")
    parser.add_argument("-p", "--password", help="Password for authentication")

    args = parser.parse_args()

    print('Python %s on %s' % (sys.version.replace('\n', ' '), sys.platform.replace('\n', ' ')))

    doit(args)

    return 0


if __name__ == "__main__":
    sys.exit(main())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Only subscriptions are sync, no listened episodes

5 participants