MLS Deeplink

MLSDeeplink – Tiny MLS M3U/XMLTV feeder with Apple TV deep links

I’ve been messing around with MLS on Apple TV and ended up building a little side project called MLSDeeplink.

It does one thing: grabs the MLS schedule from Apple TV MLS package and spits out:

  • mls.m3u – M3U playlist
  • guide.xml – XMLTV guide
  • mls_schedule.json – cleaned-up schedule
  • raw_canvas.json – raw scrape if you want to poke at it

Everything lives in a single Docker container with a built-in daily job and NGINX serving the files from /out.

Repo / Download

Code + Docker setup are here:

https://github.com/kineticman/MLSDeeplink

How it works (quick version)

  1. Container starts up
  2. A daily job runs at whatever time you pick (default 04:17 in your timezone)
  3. It refreshes the MLS schedule and rewrites the M3U/XMLTV
  4. NGINX serves everything over HTTP

So you end up with URLs like:

  • http://myhost.local:8096/mls.m3u
  • http://myhost.local:8096/guide.xml

which you can plug straight into Channels as a custom source (as a Streamlink). The entries deep-link into the Apple TV app.

Quick start (Docker)

git clone https://github.com/kineticman/MLSDeeplink.git
cd MLSDeeplink

# tweak if you want, or just use as-is
export HOST_PORT=8096        # host port
export PORT=8096             # internal NGINX port
export TZ=America/New_York   # timezone for the daily run
export RUN_AT=04:17          # daily scrape time (HH:MM)
export OUTPUT_DIR=/out       # where files are written in the container

docker compose up -d --build

Health check:

curl -sS http://localhost:8096/health && echo

Force a first run instead of waiting for the schedule:

docker exec -it mlsdeeplink bash -lc 'cd /app/scripts && OUTPUT_DIR=/out ./generate.sh && ./validate.sh && ls -l /out'

Using it with Channels

If your box is myhost.local and you kept HOST_PORT=8096:

  • M3U URL: http://myhost.local:8096/mls.m3u
  • XMLTV URL: http://myhost.local:8096/guide.xml

That’s it. If anyone tries it and runs into weird edge cases, I’m happy to tweak it. Not much season left so not a ton of opportunity to test it. Go Crew.

4 Likes

This is cool but I'm getting this error while building locally

Error response from daemon: Head "https://ghcr.io/v2/kineticman/mlsdeeplink/manifests/latest": denied

whoops had the package set as private-- try again pls

And I get a new error

ln: failed to create symbolic link '/etc/localtime': Permission denied

Is container running regardless of that error?

Nope. It keeps restarting

Try a fresh pull pls. Quick edit to docker entry point.

If u still get restart pls send me log.


docker ps -a --filter name=mls


docker logs mlsappletv | head -n 80

Another new error

/entrypoint.sh: line 19: /etc/nginx/nginx.conf: Permission denied

[entrypoint] ERROR: envsubst failed to render nginx.conf

And...another after an update

[entrypoint] WARNING: /etc/nginx/nginx.conf not writable; using /tmp/nginx.conf instead

2025/11/22 21:16:19 [warn] 8#8: the "user" directive makes sense only if the master process runs with super-user privileges, ignored in /tmp/nginx.conf:1

nginx: the configuration file /tmp/nginx.conf syntax is ok

2025/11/22 21:16:19 [emerg] 8#8: open() "/var/run/nginx.pid" failed (13: Permission denied)

nginx: configuration file /tmp/nginx.conf test failed

[entrypoint] ERROR: nginx config test failed

user  www-data;

worker_processes  auto;

error_log  /var/log/nginx/error.log warn;

pid        /var/run/nginx.pid;

events { worker_connections 1024; }

http {

  include       /etc/nginx/mime.types;

  default_type  application/octet-stream;

  sendfile on;

  keepalive_timeout 65;

  server_tokens off;

  server {

    listen       8096;

    server_name  _;

    access_log  /var/log/nginx/access.log;

    root /out;

    autoindex on;

    location = /health { return 200 'ok'; add_header Content-Type text/plain; }

    location / {

      try_files $uri $uri/ =404;

    }

  }

}

your killin me smalls on here and ESPN!. i actually do appreciate the feedback - its just a hobby for me.

what way are you installing this? porttainer i assume? i guess i should have tested it that way before sending out. I only tested on a git clone method.

try this docker compose for me pls:


services:
  mlsdeeplink:
    image: ghcr.io/kineticman/mlsdeeplink:latest
    container_name: mlsdeeplink

    # Change the left side (8096) if you want a different host port
    ports:
      - "8096:8096"

    environment:
      TZ: "America/New_York"   # or your timezone
      PORT: "8096"             # internal NGINX port (leave as 8096)
      RUN_AT: "04:17"          # daily scrape time (HH:MM in TZ)
      OUTPUT_DIR: "/out"       # where files are written in the container

    volumes:
      # Named volumes (or swap to bind mounts if you prefer)
      - mlsdeeplink_out:/out
      - mlsdeeplink_logs:/logs

    restart: unless-stopped

volumes:
  mlsdeeplink_out:
  mlsdeeplink_logs:

type or paste code here
1 Like

Hmm...well...for some reason that worked. Yeah...I usually like using portainer to install stuff. Easy to keep an eye on the containers when away

seems like most people on here do. ill update instructions. tnx for trying it out and debugging.

1 Like