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

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!

2 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.

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

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.

Another example of something I'll be doing for the Channels specific version -- which is to add dependencies we'd need. Exec into the container and run microdnf update followed by microdnf install jq

That worked. Thank you kind sir.

I see from previous threads you've messed around with manual recordings. How does this look to you as a concept for OliveTin?:



I'll add the recording image URL as a field, and of course, the cURL command to post the json. But it was easy to put together, and looks like it'd do the job without platform-specific stuff other solutions are employing.

EDIT: Like all of the OliveTin scripts I've written so far -- amazingly simple:
manualrecordings.sh:

#!/bin/bash

# Define the values
name="$1"
channel="$2"
time=$(date -d "$(date +'%Y-%m-%d') $3" +%s)
duration=$(($4 * 60))
summary="$5"

source="manual"
image="https://tmsimg.fancybits.co/assets/p9467679_st_h6_aa.jpg 2"
raw=""

# Create the JSON content
json_content=$(cat <<EOF
{
    "Name": "$name",
    "Time": $time,
    "Duration": $duration,
    "Channels": ["$channel"],
    "Airing": {
        "Source": "$source",
        "Channel": "$channel",
        "Time": $time,
        "Duration": $duration,
        "Title": "$name",
        "Summary": "$summary",
        "Image": "$image",
        "Raw": "$raw"
    }
}
EOF
)

# Output the JSON content to a file
echo "$json_content" > /config/output.json

cat /config/output.json

And the config.yaml snippet:

  - title: Manually Add Recordings
    icon: '<img src = "https://community-assets.getchannels.com/original/2X/5/55232547f7e8f243069080b6aec0c71872f0f537.png" width = "48px"/>'
    shell: /config/manualrecordings.sh "{{ name }}" {{ channel }} {{ time }} {{ duration }} "{{ summary }}"
    arguments:
      - name: name
        type: ascii_sentence
        description: The name you'd like used for the recording
      - name: channel
        type: int
        description: The channel number to use for the recording
      - name: time
        type: very_dangerous_raw_string
        description: The time to start the recording in 24h format hh:mm
      - name: duration
        type: int
        description: The length of the recording in minutes
      - name: summary
        type: ascii_sentence
        description: A Description of the recording
1 Like

Great job, @bnhf ! :clap:

1 Like

Just wanted to point out a couple things.
Looks like you copy/pasted from a post which shows 2 clicks on a url, hence you have a space and number 2 where they don't belong.

It won't hurt anything leaving "Raw": "" in the json, but it's not needed

Thanks, I appreciate the feedback. I changed that temporary hard-coded URL to an image field and the config.yaml snippet looks like this:

  - title: Manually Add Recordings
    icon: '<img src = "https://community-assets.getchannels.com/original/2X/5/55232547f7e8f243069080b6aec0c71872f0f537.png" width = "48px"/>'
    shell: /config/manualrecordings.sh "{{ name }}" {{ channel }} {{ time }} {{ duration }} "{{ summary }}" {{ image }}
    arguments:
      - name: name
        type: ascii_sentence
        description: The name you'd like used for the recording
      - name: channel
        type: int
        description: The channel number to use for the recording
      - name: time
        type: very_dangerous_raw_string
        description: The time to start the recording in 24h format hh:mm
      - name: duration
        type: int
        description: The length of the recording in minutes
      - name: summary
        type: ascii_sentence
        description: A Description of the recording (optional)
      - name: image
        type: url
        description: An image URL to use for the recording (http://$CHANNELS_DVR/admin/recordings/files/uploads to add images and get URL)
        default: http://$CHANNELS_DVR/admin/recordings/files/uploads/your_image_number/content

With the idea that one would add their desired image to DVR-Manage-Images and then replace /your_image_number/ with the actual image number. We could also have a field for just the image number, but it seems reasonable to allow for a URL from outside of Channels DVR too.

I'll get rid of "raw" as I wasn't planning on adding a field for that anyway.

Great!

I imagine some users will want to schedule things a day or more in advance, so you might consider changing your time input to accept a date/time.

@racameron did a nice job with the script he created here

I created a quick and dirty macro in Keyboard Maestro and it filled out more of the possible fields for a manual recording. Here is the user input panel.

After converting the duration from minutes to seconds I wrote it out to a temp file and passed the json as a parameter in the curl command.
You have captured the important parameters for the manual recording. I don't know how to do it in Olivetin but setting the date/time could be made easier.

OK, I think we're in business! I've built a Channels specific version of OliveTin including a config.yaml and supporting scripts.

Currently it will do the following:

  • Turn off Comskip for selected channels
  • Turn Comskip back on for selected channels
  • List the Channels with Comskip turned off
  • Create manual recordings based on channel number, date and time
  • List Gracenote Station IDs for a given search string

Here's the docker-compose for Portainer-Stacks:

version: '3.9'
services:
  olivetin:
    image: bnhf/olivetin:latest
    container_name: olivetin
    ports:
      - 1337:1337
    environment:
      - CHANNELS_DVR=${CHANNELS_DVR} # Add your Channels DVR server in the form hostname:port or ip:port
      - 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
    volumes:
      - /data/olivetin:/config # replace host path or volume as needed
    restart: unless-stopped

And the some example environment variables:

Once it's up-and-running you should be able to find the WebUI at http://<your_docker_host>:1337. Let me know how it goes with what I've defined so far, and feel free to suggest additions of existing scripts or other command line stuff you use for Channels DVR.