Skip to content

Commit

Permalink
[WIP] Initial copy of Plex to add Channels
Browse files Browse the repository at this point in the history
Copied Plex config to Channels folder and initial work
  • Loading branch information
zchrykng committed Feb 2, 2025
1 parent e3b4205 commit 92ab85b
Show file tree
Hide file tree
Showing 66 changed files with 6,899 additions and 0 deletions.
3 changes: 3 additions & 0 deletions ix-dev/community/channels-dvr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Channels

[Chanels](https://getchannels.com/) is a media server that allows you to record broadcast television.
53 changes: 53 additions & 0 deletions ix-dev/community/channels-dvr/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
app_version: 1.41.2.9200-c6bbc1b53
capabilities:
- description: Channels is able to chown files.
name: CHOWN
- description: Channels is able to bypass permission checks for it's sub-processes.
name: FOWNER
- description: Channels is able to bypass permission checks.
name: DAC_OVERRIDE
- description: Channels is able to set group ID for it's sub-processes.
name: SETGID
- description: Channels is able to set user ID for it's sub-processes.
name: SETUID
- description: Channels is able to kill processes.
name: KILL
categories:
- media
description:
Channels is a media server that allows you to record broadcast television
client.
home: https://plex.tv
host_mounts: []
icon: https://media.sys.truenas.net/apps/channels-dvr/icons/icon.png
keywords:
- channels
- media
- entertainment
- movies
- series
- tv
- streaming
- dvr
lib_version: 2.0.15
lib_version_hash: 556237781bb6a44e6b255244944456151dee1d05de2b6123dfec3f0ded635570
maintainers:
- email: [email protected]
name: truenas
url: https://www.truenas.com/
name: plex
run_as_context:
- description: Channels runs as root user.
gid: 0
group_name: root
uid: 0
user_name: root
screenshots:
- https://media.sys.truenas.net/apps/channels-dvr/screenshots/screenshot1.png
- https://media.sys.truenas.net/apps/channels-dvr/screenshots/screenshot2.png
sources:
- https://getchannels.com
- https://hub.docker.com/r/fancybits/channels-dvr
title: Channels
train: stable
version: 1.0.0
15 changes: 15 additions & 0 deletions ix-dev/community/channels-dvr/item.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
categories:
- media
icon_url: https://media.sys.truenas.net/apps/channels-dvr/icons/icon.png
screenshots:
- https://media.sys.truenas.net/apps/channels-dvr/screenshots/screenshot1.png
- https://media.sys.truenas.net/apps/channels-dvr/screenshots/screenshot2.png
tags:
- channels
- media
- entertainment
- movies
- series
- tv
- streaming
- dvr
12 changes: 12 additions & 0 deletions ix-dev/community/channels-dvr/ix_values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
images:
image:
repository: fancybits/channels-dvr
tag: latest
nvidia:
repository: fancybits/channels-dvr
tag: nvidia

consts:
plex_container_name: channels-dvr
perms_container_name: permissions
internal_web_port: 32400
Empty file.
70 changes: 70 additions & 0 deletions ix-dev/community/channels-dvr/migrations/migrate_from_kubernetes
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/python3

import os
import sys
import yaml

from migration_helpers.resources import migrate_resources
from migration_helpers.dns_config import migrate_dns_config
from migration_helpers.storage import migrate_storage_item


def migrate(values):
config = values.get("helm_secret", {}).get("config", {})
if not config:
raise ValueError("No config found in values")

envs = []
allowed_networks = []

for env in config["plexConfig"].get("additionalEnvs", []):
if env["name"] == "ALLOWED_NETWORKS":
allowed_networks = env["value"].split(",")
elif env["name"] != "NVIDIA_VISIBLE_DEVICES":
envs.append(env)

new_values = {
"plex": {
"additional_envs": envs,
"claim_token": config["plexConfig"].get("claimToken", ""),
"allowed_networks": allowed_networks,
"image_selector": (
"image"
if config["plexConfig"]["imageSelector"] == "image"
else "plex_pass_image"
),
},
"run_as": {
"user": config["plexID"].get("user", 568),
"group": config["plexID"].get("group", 568),
},
"network": {
"host_network": config["plexNetwork"].get("hostNetwork", False),
"web_port": config["plexNetwork"].get("webPort", 32400),
"dns_opts": migrate_dns_config(config["podOptions"].get("dnsConfig", {})),
},
"storage": {
"config": migrate_storage_item(config["plexStorage"]["config"]),
"data": migrate_storage_item(config["plexStorage"]["data"]),
"logs": migrate_storage_item(config["plexStorage"]["logs"]),
"transcode": migrate_storage_item(config["plexStorage"]["transcode"]),
"additional_storage": [
migrate_storage_item(item, include_read_only=True)
for item in config["plexStorage"]["additionalStorages"]
],
},
"resources": migrate_resources(
config["resources"], config["plexGPU"], values.get("gpu_choices", {})
),
}

return new_values


if __name__ == "__main__":
if len(sys.argv) != 2:
exit(1)

if os.path.exists(sys.argv[1]):
with open(sys.argv[1], "r") as f:
print(yaml.dump(migrate(yaml.safe_load(f.read()))))
Empty file.
30 changes: 30 additions & 0 deletions ix-dev/community/channels-dvr/migrations/migration_helpers/cpu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import math
import re
import os

CPU_COUNT = os.cpu_count()

NUMBER_REGEX = re.compile(r"^[1-9][0-9]$")
FLOAT_REGEX = re.compile(r"^[0-9]+\.[0-9]+$")
MILI_CPU_REGEX = re.compile(r"^[0-9]+m$")


def transform_cpu(cpu) -> int:
result = 2
if NUMBER_REGEX.match(cpu):
result = int(cpu)
elif FLOAT_REGEX.match(cpu):
result = int(math.ceil(float(cpu)))
elif MILI_CPU_REGEX.match(cpu):
num = int(cpu[:-1])
num = num / 1000
result = int(math.ceil(num))

if CPU_COUNT is not None:
# Do not exceed the actual CPU count
result = min(result, CPU_COUNT)

if int(result) == 0:
result = CPU_COUNT if CPU_COUNT else 2

return int(result)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
def migrate_dns_config(dns_config):
if not dns_config:
return []

dns_opts = []
for opt in dns_config.get("options", []):
dns_opts.append(f"{opt['name']}:{opt['value']}")

return dns_opts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
def get_value_from_secret(secrets=None, secret_name=None, key=None):
secrets = secrets if secrets else dict()
secret_name = secret_name if secret_name else ""
key = key if key else ""

if not secrets or not secret_name or not key:
raise ValueError("Expected [secrets], [secret_name] and [key] to be set")
for curr_secret_name, curr_data in secrets.items():
if curr_secret_name.endswith(secret_name):
if not curr_data.get(key, None):
raise ValueError(
f"Expected [{key}] to be set in secret [{curr_secret_name}]"
)
return curr_data[key]

raise ValueError(f"Secret [{secret_name}] not found")
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import re
import math
import psutil

TOTAL_MEM = psutil.virtual_memory().total

SINGLE_SUFFIX_REGEX = re.compile(r"^[1-9][0-9]*([EPTGMK])$")
DOUBLE_SUFFIX_REGEX = re.compile(r"^[1-9][0-9]*([EPTGMK])i$")
BYTES_INTEGER_REGEX = re.compile(r"^[1-9][0-9]*$")
EXPONENT_REGEX = re.compile(r"^[1-9][0-9]*e[0-9]+$")

SUFFIX_MULTIPLIERS = {
"K": 10**3,
"M": 10**6,
"G": 10**9,
"T": 10**12,
"P": 10**15,
"E": 10**18,
}

DOUBLE_SUFFIX_MULTIPLIERS = {
"Ki": 2**10,
"Mi": 2**20,
"Gi": 2**30,
"Ti": 2**40,
"Pi": 2**50,
"Ei": 2**60,
}


def transform_memory(memory):
result = 4096 # Default to 4GB

if re.match(SINGLE_SUFFIX_REGEX, memory):
suffix = memory[-1]
result = int(memory[:-1]) * SUFFIX_MULTIPLIERS[suffix]
elif re.match(DOUBLE_SUFFIX_REGEX, memory):
suffix = memory[-2:]
result = int(memory[:-2]) * DOUBLE_SUFFIX_MULTIPLIERS[suffix]
elif re.match(BYTES_INTEGER_REGEX, memory):
result = int(memory)
elif re.match(EXPONENT_REGEX, memory):
result = int(float(memory))

result = math.ceil(result)
result = min(result, TOTAL_MEM)
# Convert to Megabytes
result = result / 1024 / 1024

if int(result) == 0:
result = TOTAL_MEM if TOTAL_MEM else 4096

return int(result)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from .memory import transform_memory, TOTAL_MEM
from .cpu import transform_cpu, CPU_COUNT


def migrate_resources(resources, gpus=None, system_gpus=None):
gpus = gpus or {}
system_gpus = system_gpus or []

result = {
"limits": {
"cpus": int((CPU_COUNT or 2) / 2),
"memory": int(TOTAL_MEM / 1024 / 1024),
}
}

if resources.get("limits", {}).get("cpu", ""):
result["limits"].update(
{"cpus": transform_cpu(resources.get("limits", {}).get("cpu", ""))}
)
if resources.get("limits", {}).get("memory", ""):
result["limits"].update(
{"memory": transform_memory(resources.get("limits", {}).get("memory", ""))}
)

gpus_result = {}
for gpu in gpus.items() if gpus else []:
kind = gpu[0].lower() # Kind of gpu (amd, nvidia, intel)
count = gpu[1] # Number of gpus user requested

if count == 0:
continue

if "amd" in kind or "intel" in kind:
gpus_result.update({"use_all_gpus": True})
elif "nvidia" in kind:
sys_gpus = [
gpu_item
for gpu_item in system_gpus
if gpu_item.get("error") is None
and gpu_item.get("vendor", None) is not None
and gpu_item.get("vendor", "").upper() == "NVIDIA"
]
for sys_gpu in sys_gpus:
if count == 0: # We passed # of gpus that user previously requested
break
guid = sys_gpu.get("vendor_specific_config", {}).get("uuid", "")
pci_slot = sys_gpu.get("pci_slot", "")
if not guid or not pci_slot:
continue

gpus_result.update(
{"nvidia_gpu_selection": {pci_slot: {"uuid": guid, "use_gpu": True}}}
)
count -= 1

if gpus_result:
result.update({"gpus": gpus_result})

return result
Loading

0 comments on commit 92ab85b

Please sign in to comment.