OliveTin for Channels: An Interface for Misc Channels DVR Scripts & Tricks

I've been working on a Channels DVR specific version of the OliveTin project. The idea is to make this a central interface for the various scripts people have developed, and miscellaneous tricks that require Python, cURL, jq or whatever. I'll build and maintain the container, including whatever dependencies are required for your favorite script or command line magic for Channels DVR.

Here are some screenshots of a few things I have running so far, just to give you an idea of what's possible. I'd like to get some feedback on this idea, and whatever suggestions anyone might have for things to add. OliveTin can do all kinds of stuff, and lends itself quite well to tablets and phones. It's also pretty great for automating stuff that you might want someone else in your family to be able to do in a pinch:





You'll be able to add your own stuff of course, but anything that others might want to do I'd like to include in the container. This will be strictly a Docker with Portainer project, and will run on the same processors as Channels DVR. The interface is super-simple, but supports multiple arguments of various kinds, and produces standard output and error output in log form.

EDIT 2023.12.28 -

Here's the current list of "Actions" supported by OliveTin-for-Channels:

  • Comskip on/off by channel
  • List channels with Comskip off
  • Manually add recordings
  • Find Gracenote station IDs
  • Fix YouTube thumbnails
  • Download TwiT.tv guide data
  • Generate channels list in CSV format
  • Generate movie list in CSV format
  • Scan/Prune local content
  • Mark an episode for re-recording
  • Channel lineup change notifications
  • Generate filtered Channels DVR log
  • List sources for a single channel
  • Delete Channels DVR recording log files
  • Send message to defined Channels clients
  • Docker-Compose Examples for Channels & Related Extensions
  • Remove Comskip markers from a recording
  • Restart or shutdown a Channels DVR server
  • Ping Channels DVR server
  • Generate a Channels DVR M3U playlist
  • Remove commercials based on an EDL file
  • Create EDL file from PlayOn recording chapters
  • Create subtitles (.srt) file from Closed Captions
  • Update Commercials Metadata from LosslessCut LLC File

And, here's what the interface looks like as of the edit date:

Here's the current recommended docker-compose (this project is supported through Portainer):

version: '3.9'
services:
  olivetin: # This docker-compose requires little or no editing. Set the Environment variables section of Portainer.
    # 2024.10.20
    # GitHub home for this project: https://github.com/bnhf/OliveTin.
    # Docker container home for this project with setup instructions: https://hub.docker.com/repository/docker/bnhf/olivetin.
    image: bnhf/olivetin:${TAG} # Add the tag like latest or test to the environment variables below.
    container_name: olivetin
    hostname: olivetin
    dns_search: ${DOMAIN} # For Tailscale users using Magic DNS, add your Tailnet (tailxxxxx.ts.net) to use hostnames for remote nodes, otherwise use your local domain name.
    ports:
      - ${HOST_PORT}:1337
    environment:
      - CHANNELS_DVR=${CHANNELS_DVR_HOST}:${CHANNELS_DVR_PORT} # Add your Channels DVR server in the form CHANNELS_DVR_HOST=<hostname or ip> and CHANNELS_DVR_PORT=<port>.
      #- CHANNELS_DVR_ALTERNATES=${CHANNELS_DVR2_HOST}:${CHANNELS_DVR2_PORT} # Space separated list of alternate Channels DVR servers to choose from in the form hostname:port or ip:port.
      - CHANNELS_CLIENTS=${CHANNELS_CLIENTS} # Space separated list of Channels DVR clients you'd like notifications sent to in the form hostname or IP.
      - ALERT_SMTP_SERVER=${ALERT_SMTP_SERVER} # SMTP server to use for sending alert e-mails. smtp.gmail.com:587 for example.
      - ALERT_EMAIL_FROM=${ALERT_EMAIL_FROM} # Sender address for alert e-mails.
      - ALERT_EMAIL_PASS=${ALERT_EMAIL_PASS} # SMTP "app" password established through GMail or Yahoo Mail. Do not use your everyday e-mail address.
      - ALERT_EMAIL_TO=${ALERT_EMAIL_TO} # Recipient address for alert e-mails.
      - UPDATE_YAMLS=${UPDATE_YAMLS} # Set this to true to update config.yaml.
      - UPDATE_SCRIPTS=${UPDATE_SCRIPTS} # Set this to true to update all included scripts.
      - TZ=${TZ} # Add your local timezone in standard linux format. E.G. US/Eastern, US/Central, US/Mountain, US/Pacific, etc.
      - PORTAINER_TOKEN=${PORTAINER_TOKEN} # Generate via <username> dropdown (upper right of WebUI), "My account", API tokens.
      - PORTAINER_HOST=${PORTAINER_HOST} # Hostname or IP of the Docker host you're running Portainer on.
    volumes:
      - ${HOST_DIR}/olivetin:/config # Add the parent directory on your Docker you'd like to use.
      - ${DVR_SHARE}:/mnt/${CHANNELS_DVR_HOST}-${CHANNELS_DVR_PORT} # This can either be a Docker volume or a host directory that's connected via Samba or NFS to your Channels DVR network share.
      - ${LOGS_SHARE}:/mnt/${CHANNELS_DVR_HOST}-${CHANNELS_DVR_PORT}_logs # This can either be a Docker volume or a host directory that's connected via Samba or NFS to your Channels DVR logs network share.
      #- ${DVR2_SHARE}:/mnt/${CHANNELS_DVR2_HOST}-${CHANNELS_DVR2_PORT} # Note that these volume mounts should always be to /mnt/hostname-port or /mnt/ip-port (dash rather than a colon between).
      #- ${LOGS2_SHARE}:/mnt/${CHANNELS_DVR2_HOST}-${CHANNELS_DVR2_PORT}_logs # This can either be a Docker volume or a host directory that's connected via Samba or NFS to your Channels DVR logs network share.
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped

  static-file-server:
    image: halverneus/static-file-server:latest
    container_name: static-file-server
    dns_search: ${DOMAIN}
    ports:
      - ${HOST_SFS_PORT}:8080
    environment:
      - FOLDER=${FOLDER}
    volumes:
      - ${HOST_DIR}/olivetin/data:${FOLDER}
    restart: unless-stopped

#volumes: # Use this section if you've setup docker volumes named channels-dvr and channels-dvr-logs, with CIFS or NFS, to bind to /mnt/${CHANNELS_DVR_HOST}-${CHANNELS_DVR_PORT} and  /mnt/${CHANNELS_DVR_HOST}-${CHANNELS_DVR_PORT}_logs inside the container. Set DVR_SHARE=channels-dvr and LOGS_SHARE=channels-dvr-logs in this example.
  #channels-dvr:
    #external: true
  #channels-dvr-logs:
    #external: true

And some sample environment variables:

TAG=latest
DOMAIN=tailxxxxx.ts.net
HOST_PORT=1337
CHANNELS_DVR_HOST=local-server
CHANNELS_DVR_PORT=8089
CHANNELS_DVR2_HOST=another-server
CHANNELS_DVR2_PORT=8089
CHANNELS_CLIENTS=appletv4k-den firestick-bedroom
ALERT_SMTP_SERVER=smtp.gmail.com:587
ALERT_EMAIL_FROM=username@gmail.com
ALERT_EMAIL_PASS=xxxxxxxxxxxxxxxx
ALERT_EMAIL_TO=username@gmail.com
UPDATE_YAMLS=true
UPDATE_SCRIPTS=true
TZ=US/Mountain
HOST_DIR=/data
DVR_SHARE=/mnt/dvr
LOGS_SHARE=/mnt/channelsdvr
HOST_SFS_PORT=8080
FOLDER=/web
PORTAINER_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PORTAINER_HOST=docker-host

If you're not familiar with binding directories or Docker Volumes to a Docker Container, here's a post with some additional detail:

For those that haven't used Portainer before (highly recommended as a WebUI for Docker!), here's the command line install for it:

docker run -d -p 8000:8000 -p 9000:9000 -p 9443:9443 --name portainer \
    --restart=always \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v portainer_data:/data \
    cr.portainer.io/portainer/portainer-ce:latest

And, here's step-by-step video I put together on installing OliveTin-for-Channels:

7 Likes

That looks really great. Have to got a link to the project?

1 Like

Wow, OliveTin is one cool tool I didn't know existed.
Could see it it saving lots of manual work.
Like search for station id's and create a custom channel m3u when trying to find which one matches a stream.
I've had to do that all manually, like finding the right one for Dabl Guide data doesn't match what's playing - #2 by chDVRuser

#EXTM3U
#EXTINF:-1 channel-number="1011" tvc-guide-stationid="112157" channel-id="DABL" tvg-name="DABL",DABL
http://192.168.1.4:8089/devices/ANY/channels/6781/stream.mpg?format=ts
#EXTINF:-1 channel-number="1012" tvc-guide-stationid="120761" channel-id="DABLHD" tvg-name="DABLHD",DABLHD
http://192.168.1.4:8089/devices/ANY/channels/6781/stream.mpg?format=ts
#EXTINF:-1 channel-number="1013" tvc-guide-stationid="112158" channel-id="DABLP" tvg-name="DABLP",DABLP
http://192.168.1.4:8089/devices/ANY/channels/6781/stream.mpg?format=ts
#EXTINF:-1 channel-number="1014" tvc-guide-stationid="120762" channel-id="DABLPHD" tvg-name="DABLPHD",DABLPHD
http://192.168.1.4:8089/devices/ANY/channels/6781/stream.mpg?format=ts
#EXTINF:-1 channel-number="1015" tvc-guide-stationid="130094" channel-id="DABLSTR" tvg-name="DABLSTR",DABLSTR
http://192.168.1.4:8089/devices/ANY/channels/6781/stream.mpg?format=ts
#EXTINF:-1 channel-number="1016" tvc-guide-stationid="131032" channel-id="DABLPP" tvg-name="DABLPP",DABLPP
http://192.168.1.4:8089/devices/ANY/channels/6781/stream.mpg?format=ts

and previously for some FrndlyTV channels Frndly TV for Channels - #694 by chDVRuser

4 Likes
4 Likes

I haven't built the Channels specific version yet -- I'm looking for input at this stage on what to include.

If you have specific scripts, use cURL commands, or have command line stuff you execute every now-and-then related to Channels DVR let me know. Things that are useful enough to CDVR users will be added!

1 Like

Yeah, it's super easy to use -- for example here's the yaml and a sample script for what I've done so far:

actions:
  - title: Find Gracenote Station IDs
    icon: '<img src = "https://community-assets.getchannels.com/original/2X/5/55232547f7e8f243069080b6aec0c71872f0f537.png" width = "48px"/>'
    shell: /config/stationid.sh "{{ station }}"
    arguments:
      - name: station
        type: ascii_sentence

  - title: Turn Off Comskip by Channel
    icon: '<img src = "https://community-assets.getchannels.com/original/2X/5/55232547f7e8f243069080b6aec0c71872f0f537.png" width = "48px"/>'
    shell: /config/setcomskipignore.sh {{ channel }}
    arguments:
      - name: channel
        type: ascii_sentence

  - title: List Channels with Comskip Off
    icon: '<img src = "https://community-assets.getchannels.com/original/2X/5/55232547f7e8f243069080b6aec0c71872f0f537.png" width = "48px"/>'
    shell: /config/listcomskipignore.sh

And, here's an example script -- notice I lifted the main cURL/jq command right from your recent Windows script :wink::

#! /bin/bash

stationName=$(echo "$1" | sed 's/ /%20/g; s/^/%22/; s/$/%22/')

curl http://$CHANNELS_DVR/tms/stations/$stationName \
  | jq --raw-output "sort_by(.type, .name) | .[] | \"type: \(.type), name: \(.name), callSign: \(.callSign), stationId: \(.stationId), logo: \(.preferredImage.uri)\""

But it's simpler because it's Docker, and there's a basic UI to drive input.

I've forked the OliveTin project, and I'll add whatever dependencies are required for CDVR users, along with a representative config.yaml and some simple scripts like the above.

1 Like

Looks too easy. Just write some shell script and prompt for input.

Not sure if you want to add the latest addition, the affiliate call sign

1 Like

I will, thanks. Let me know if you have other stuff you've written or use that you think would be good.

I'm pretty sure the recent @mjitkop Python script for adding manual recordings would be pretty easy to add, as OliveTin supports drop-down-style fields as well. Hopefully he'll see this and weigh-in. Doing it this way would address inevitable dependency issues people have, and provide a decent, consistent interface for use.

1 Like

Totally agree. Fits what docker containers were made to do.

For anyone that wants to get going with this before I build the first Channels-specific version, here's a docker-compose suitable for Portainer-Stacks:

version: '3.9'
services:
  olivetin:
    image: jamesread/olivetin:latest
    container_name: olivetin
    ports:
      - 1337:1337
    environment:
      - CHANNELS_DVR=${CHANNELS_DVR}
    volumes:
      - /data/olivetin:/config # replace host path or volume as needed
    restart: unless-stopped

Add your Channels DVR server hostname:port or ip:port to the environemnt section, and you'll be good to go. If you need to temporarily add dependencies, exec into the container as root and do a microdnf update, followed by a microdnf install <your_package_name>. Your package will be present until the next container restart.

Let me know of anything you want included package-wise in the Channels specific fork I'll build. This will also include a Channels-oriented config.yaml file and whatever scripts we've come up with.

1 Like

And here are the 3 scripts I've done so far, which as you can see are dead simple. They're stored in the bound /data/olivetin directory for persistence:

stationid.sh:

#! /bin/bash

stationName=$(echo "$1" | sed 's/ /%20/g; s/^/%22/; s/$/%22/')

curl http://$CHANNELS_DVR/tms/stations/$stationName \
  | jq --raw-output "sort_by(.type, .name) | .[] | \"type: \(.type), name: \(.name), callSign: \(.callSign), stationId: \(.stationId), affiliate: \(.affiliateCallSign), logo: \(.preferredImage.uri)\""

setcomskipignore.sh:

#! /bin/bash

channel=$1

curl -XPUT http://$CHANNELS_DVR/comskip/ignore/channel/$channel

listcomskipignore.sh:

#! /bin/bash

curl http://$CHANNELS_DVR/settings \
  | jq 'to_entries | map(select(.key | test("comskip.ignore.channel.\\d+"))) | from_entries'

Basic, but very effective combined with OliveTin!

Hey this is super cool, I'm definitely down to try it. Seems like a great way to organize the snippets of code and scripts I have scattered about.

How about adding this good one too, would it work? It's made in Ruby. Could it run on a recurring schedule?

Incorporating this one as a docker would make things a lot easier for folks too:

Are these the sorts of miscellaneous scripts and tricks you mean? Thanks for making and sharing this! Inspiring!

3 Likes

Absolutely.

ChatGPT translated the Ruby script easily enough to Bash, but I'm not sure about having it be a cron job. I'll have to think about the options. But it could definitely be run on demand:

#!/bin/bash

# Define server URL and video group
server_url="http://YOUR_IP:8089"
video_group="VIDEO_GROUP_ID"

# Retrieve source files using curl and parse JSON
source_files=$(curl -s "${server_url}/dvr/groups/${video_group}/files")

# Iterate through each file
for file in $(echo "$source_files" | jq -c '.[]'); do
  # Extract file_id, title, and yt_match from JSON
  file_id=$(echo "$file" | jq -r '.ID')
  title=$(echo "$file" | jq -r '.Airing.EpisodeTitle')
  yt_match=$(echo "$title" | grep -oE '_\((\d{4}-\d{2}-\d{2})\))?_?\[([^]]+)\]')

  if [[ -n $yt_match ]]; then
    yt_date=$(echo "$yt_match" | sed -E 's/_\((\d{4}-\d{2}-\d{2})\))?_?\[([^]]+)\]/\1/')
    yt_id=$(echo "$yt_match" | sed -E 's/_\((\d{4}-\d{2}-\d{2})\))?_?\[([^]]+)\]/\2/')
    yt_thumbnail_url="https://i3.ytimg.com/vi/${yt_id}/maxresdefault.jpg"
    new_title=$(echo "$title" | sed -E "s/$yt_match//")

    # Construct the JSON package
    package='{"Thumbnail": "'$yt_thumbnail_url'", "Airing": {"EpisodeTitle": "'$new_title'"'

    if [[ -n $yt_date ]]; then
      package+=', "OriginalDate": "'$yt_date'"'
    fi

    package+='}}'

    # Make a PUT request using curl to update the file
    curl -X PUT -H "Content-Type: application/json" -d "$package" "${server_url}/dvr/files/${file_id}"
  fi
done

This looks doable too, as we'd certainly have Python in the container, and the dependencies look modest.

1 Like

This would be awesome too! Getting that to run on macOS was not for the faint of heart :pleading_face:

1 Like

Since you've made a way for users to set and lists comskip ignore channels, should be easy to add an option to remove those. Don't know if you want to present the current list and have them check one or multiples to remove, or just have them enter the channel number.

This looks great. :star_struck:

It's funny because I recently posted the following in GUI app to make it easy to schedule manual recordings :

So this is perfect! :smiley:

Sadly, though, life is very busy for me right now and I don't know when I will have time to get to it. :frowning:

If somebody wants to take my code and integrate it into OliveTin, be my guest! :slightly_smiling_face:

This looks pretty cool @bnhf. I'm having trouble getting the container to start in Portainer on my Synology NAS. I'm doing something stupid but I can't figure it out. The container log shows that it cannot find the config.yaml file for some reason.

level="error" msg="Config file error at startup. Config File \"config.yaml\" Not Found in \"[/ /config /etc/OliveTin]\""

This is what my compose says:

  volumes:
      - /volume1/data/olivetin:/config # replace host path or volume as needed

Here is the olivetin config directory when I shell into the NAS:

Chris@CJYNAS:/volume1/data/olivetin$ pwd
/volume1/data/olivetin
Chris@CJYNAS:/volume1/data/olivetin$ ls -las
total 20
0 drwxrwxrwx+ 1 Chris users 158 Sep 24 05:04 .
0 drwxrwxrwx+ 1 root  root   62 Sep 24 05:07 ..
4 -rwxrwxrwx+ 1 Chris users 305 Sep 24 05:54 compose.txt
4 -rwxrwxrwx+ 1 Chris users 828 Sep 24 04:56 config.yaml
0 drwxrwxrwx+ 1 root  users 276 Sep 24 05:04 @eaDir
4 -rwxrwxrwx+ 1 Chris users 144 Sep 24 04:59 listcomskipignore.sh
4 -rwxrwxrwx+ 1 Chris users  89 Sep 24 04:59 setcomskipignore.sh
4 -rwxrwxrwx+ 1 Chris users 332 Sep 24 04:58 stationid.sh

Anyone have any ideas why olivetin cannot see the config.yaml file?

2 Likes

That's an issue I'll address when I build the Channels specific version. config.yaml needs to exist in the generic version for the container to start. You can take the one I posted earlier in the thread, and put it in your bound data directory, and then start the container.

Not sure. YAML files aren't executable, but that's the only thing that jumps out at me so far.

EDIT: Maybe permissions? Try adding user: root to your docker-compose

1 Like

Yup, that was it. Added root permissions in the compose. Here is my compose now that is working. Thanks for the help.

version: '3.9'
services:
  olivetin:
    image: jamesread/olivetin:latest
    container_name: olivetin
    user: root
    ports:
      - 1337:1337
    environment:
      - CHANNELS_DVR=${CHANNELS_DVR}
    volumes:
      - /volume1/data/olivetin:/config # replace host path or volume as needed
    restart: unless-stopped

EDIT: Well the container starts but I'm getting an error that it cannot write to the destination. Where is the CURL output going?

exit status 127

/config/listcomskipignore.sh: line 4: jq: command not found
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (23) Failure writing output to destination

Another EDIT: I see the message that jq cannot be found. I'm able to execute the listcomskipignore.sh successfully if I shell into the NAS. For some reason the scope of "jq" running in Portainer is not valid.