summaryrefslogtreecommitdiffstats
path: root/linhes/linhes-system
diff options
context:
space:
mode:
Diffstat (limited to 'linhes/linhes-system')
-rw-r--r--linhes/linhes-system/10-monitor.conf10
-rw-r--r--linhes/linhes-system/79-cronie.hook9
-rw-r--r--linhes/linhes-system/81-wol.rules1
-rw-r--r--linhes/linhes-system/LinHES-release1
-rwxr-xr-xlinhes/linhes-system/PKGBUILD161
-rwxr-xr-xlinhes/linhes-system/add_storage.py1120
-rw-r--r--linhes/linhes-system/add_storage.readme3
-rwxr-xr-xlinhes/linhes-system/balance_storage_groups.py165
-rwxr-xr-xlinhes/linhes-system/be_check.py12
-rwxr-xr-xlinhes/linhes-system/checkXFSfrag.sh70
-rwxr-xr-xlinhes/linhes-system/create_media_dirs.sh74
-rwxr-xr-xlinhes/linhes-system/diskspace.sh67
-rwxr-xr-xlinhes/linhes-system/empty_storage_groups.py195
-rwxr-xr-xlinhes/linhes-system/enableIRWake.sh22
-rwxr-xr-xlinhes/linhes-system/find_orphans.py250
-rw-r--r--linhes/linhes-system/flatpak_update.cron2
-rw-r--r--linhes/linhes-system/fstrim.hook9
-rwxr-xr-xlinhes/linhes-system/gen_lib_xml.py115
-rwxr-xr-xlinhes/linhes-system/idle.py379
-rwxr-xr-xlinhes/linhes-system/jobqueue_helper.py63
-rwxr-xr-xlinhes/linhes-system/lh_apply_UI_settings.sh39
-rwxr-xr-xlinhes/linhes-system/lh_home_check.sh9
-rw-r--r--linhes/linhes-system/lh_lighttpd.conf295
-rwxr-xr-xlinhes/linhes-system/lh_log_care.cron16
-rw-r--r--linhes/linhes-system/lh_mtc.cron80
-rwxr-xr-xlinhes/linhes-system/lh_mtc.py153
-rwxr-xr-xlinhes/linhes-system/lh_myth_status.py271
-rwxr-xr-xlinhes/linhes-system/lh_notify-send12
-rw-r--r--linhes/linhes-system/lh_php.ini64
-rw-r--r--linhes/linhes-system/lh_restart_needed.hook9
-rwxr-xr-xlinhes/linhes-system/lh_restart_needed.sh39
-rwxr-xr-xlinhes/linhes-system/lh_setup_fileshare.py150
-rw-r--r--linhes/linhes-system/lh_sqlserver.cnf82
-rwxr-xr-xlinhes/linhes-system/lh_system_backup32
-rwxr-xr-xlinhes/linhes-system/lh_system_backup_job339
-rwxr-xr-xlinhes/linhes-system/lh_system_start.sh159
-rw-r--r--linhes/linhes-system/lh_system_start.sh.desktop7
-rw-r--r--linhes/linhes-system/linhes-profile.sh13
-rwxr-xr-xlinhes/linhes-system/misc_recent_recordings.pl194
-rwxr-xr-xlinhes/linhes-system/misc_status_config.py41
-rwxr-xr-xlinhes/linhes-system/misc_status_info.sh23
-rwxr-xr-xlinhes/linhes-system/misc_upcoming_recordings.pl334
-rwxr-xr-xlinhes/linhes-system/misc_which_recorder.pl107
-rwxr-xr-xlinhes/linhes-system/myth2mkv466
-rwxr-xr-xlinhes/linhes-system/myth2mp396
-rwxr-xr-xlinhes/linhes-system/myth2videos148
-rw-r--r--linhes/linhes-system/openssh.hook9
-rwxr-xr-xlinhes/linhes-system/optimize_mythdb.py16
-rw-r--r--linhes/linhes-system/paccache.cron2
-rw-r--r--linhes/linhes-system/plex_lib.conf9
-rwxr-xr-xlinhes/linhes-system/plexmediascanner.sh6
-rw-r--r--linhes/linhes-system/rc6_mce.toml70
-rw-r--r--linhes/linhes-system/readme_is_xml1
-rw-r--r--linhes/linhes-system/recordings.cron144
-rwxr-xr-xlinhes/linhes-system/remove_storage.py185
-rw-r--r--linhes/linhes-system/rsyslog.hook9
-rw-r--r--linhes/linhes-system/rsyslog.mythtv.conf63
-rw-r--r--linhes/linhes-system/system-sudo.rules2
-rwxr-xr-xlinhes/linhes-system/udev_link.sh33
-rw-r--r--linhes/linhes-system/x11vnc.override.conf8
-rw-r--r--linhes/linhes-system/xfs_defrag.cron3
61 files changed, 6466 insertions, 0 deletions
diff --git a/linhes/linhes-system/10-monitor.conf b/linhes/linhes-system/10-monitor.conf
new file mode 100644
index 0000000..a92ff49
--- /dev/null
+++ b/linhes/linhes-system/10-monitor.conf
@@ -0,0 +1,10 @@
+Section "Extensions"
+ Option "DPMS" "True"
+EndSection
+
+Section "ServerFlags"
+ Option "StandbyTime" "0"
+ Option "SuspendTime" "0"
+ Option "OffTime" "0"
+ Option "BlankTime" "0"
+EndSection
diff --git a/linhes/linhes-system/79-cronie.hook b/linhes/linhes-system/79-cronie.hook
new file mode 100644
index 0000000..59ca25e
--- /dev/null
+++ b/linhes/linhes-system/79-cronie.hook
@@ -0,0 +1,9 @@
+[Trigger]
+Operation = Install
+Type = Package
+Target = linhes-system
+
+[Action]
+Description = Enable and start cronie...
+When = PostTransaction
+Exec = /usr/bin/systemctl enable --now cronie.service
diff --git a/linhes/linhes-system/81-wol.rules b/linhes/linhes-system/81-wol.rules
new file mode 100644
index 0000000..f4d0116
--- /dev/null
+++ b/linhes/linhes-system/81-wol.rules
@@ -0,0 +1 @@
+ACTION=="add", SUBSYSTEM=="net", NAME=="enp*", RUN+="/usr/bin/ethtool -s $name wol g"
diff --git a/linhes/linhes-system/LinHES-release b/linhes/linhes-system/LinHES-release
new file mode 100644
index 0000000..e6aa8f2
--- /dev/null
+++ b/linhes/linhes-system/LinHES-release
@@ -0,0 +1 @@
+LinHES R9.0.0 (Been a minute...)
diff --git a/linhes/linhes-system/PKGBUILD b/linhes/linhes-system/PKGBUILD
new file mode 100755
index 0000000..c87bfb6
--- /dev/null
+++ b/linhes/linhes-system/PKGBUILD
@@ -0,0 +1,161 @@
+pkgname=linhes-system
+pkgver=9.0.0
+pkgrel=77
+arch=('x86_64')
+#install=$pkgname.install
+pkgdesc="Everything that makes LinHES a system"
+license=('GPL2')
+depends=('cronie' 'dbus-python' 'dvb-firmware' 'expect' 'flatpak' 'firefox' 'glances' 'inetutils'
+ 'kdialog' 'libnotify'
+ 'logrotate' 'linhes-templates' 'linhes-theme' 'mlocate' 'ncdu' 'python-dateutil' 'python-pytz'
+ 'python-tzlocal' 'openssh' 'pacman-contrib' 'rsyslog' 'ttf-overlock' 'wget' 'x11vnc'
+ 'intel-media-driver' 'libva-intel-driver' 'libva-vdpau-driver' 'pigz')
+binfiles="add_storage.py balance_storage_groups.py empty_storage_groups.py remove_storage.py
+ checkXFSfrag.sh enableIRWake.sh idle.py lh_system_start.sh lh_notify-send
+ lh_home_check.sh lh_myth_status.py lh_system_backup lh_system_backup_job
+ jobqueue_helper.py gen_lib_xml.py lh_setup_fileshare.py lh_restart_needed.sh
+ diskspace.sh find_orphans.py optimize_mythdb.py lh_mtc.py lh_apply_UI_settings.sh
+ misc_recent_recordings.pl misc_status_config.py misc_status_info.sh
+ misc_upcoming_recordings.pl misc_which_recorder.pl plexmediascanner.sh
+ create_media_dirs.sh be_check.py
+ myth2mkv myth2mp3 myth2videos udev_link.sh"
+source=($binfiles
+ 'lh_mtc.cron' 'paccache.cron' 'flatpak_update.cron' 'xfs_defrag.cron'
+ 'readme_is_xml' 'add_storage.readme' 'LinHES-release' 'lh_log_care.cron'
+ '79-cronie.hook' 'fstrim.hook' 'openssh.hook' 'plex_lib.conf' 'rsyslog.hook'
+ '10-monitor.conf' '81-wol.rules' 'x11vnc.override.conf' 'lh_lighttpd.conf' 'lh_php.ini'
+ 'system-sudo.rules' 'linhes-profile.sh' 'lh_sqlserver.cnf' 'lh_restart_needed.hook'
+ 'lh_system_start.sh.desktop' 'rc6_mce.toml' 'recordings.cron' 'rsyslog.mythtv.conf')
+sha256sums=('e6eaa2fb4819fa60cb05b4d2e7328d2914af3a73028a735ec4d56e7ece33ecc0'
+ '1115809a2d80c1ead7cfc5df05e1d6427255912a8303594574b3be23d3d3e4f1'
+ '97fe9e851c782fa9f85c5b69b110ccff2817dd4fa2a6d9ff6ee225dc558677e4'
+ 'ff261f41efec8a9963f9f59100cbe75f015028a2ed3a863ce0cb473f2ebb7b76'
+ '8b54c31b8efde3917f603c5307bebb0a2a00239ad9a983c2f5d8120003256449'
+ 'ae34515e144830f424d3bd3f6b1b446892d62beed20bca6f0fb19b0bbb779f27'
+ '5e6d128f879b0fe7c1a190cccd75d4e5d00afc161f3bc9e92ffa2d87242cc9df'
+ '3db3620b3430d1e8031596efa9eb50dea7f4c1b837c37929c3743c1eb65ac666'
+ '6d4fb0ed1a5ed961b3a3884dce093118e50c2981a9cd5837d20abc5a6d4fd8aa'
+ '87875d9e5f5ce18208f419698ce69b6bcbcd08955a57a4a13940e715af58b787'
+ '93d664f4a46fda05d0f754d19df40cbda120e325b80c07092345b14763993833'
+ 'a523388ffdc6f30f2dd2ef4803b21bded8a35bfded499dc66b786adec71840a3'
+ '6ea7f807a29631e33629da14e7ca6481310f0416c5d6f8fb415a76fa90fb9b76'
+ '91bdec992bb2c933e15625c181f2195c402060b879168ebf35944cb064c904b9'
+ '5cacfdd02833e5a3130d765573e772e6bd5030336ba86239c5e4db5ffa36fc69'
+ 'd51253c207d3315997d7122cb774c6d0fc38975c33e66bf416f0b1b95cf86e5b'
+ '52eef974c7e530d60799ee2b2c67cedc3e0c4d58948f021a609c20cfedd2056e'
+ 'ebdb3ee0212e0cc72526bb5e50a032573e1894acb7bf75617243b0b49aa1f8f2'
+ 'cd3306991c8b63f5402c0e52405cc1a6581cb3c42af74b7c2f879c8192f95610'
+ 'e371c6a289c68fe200d7da856c20a8c579efa23178f4d62235f7359d7f6e49a1'
+ 'ee9a4df83e463e0c8a2e862680a83cff07a36cf3606faf60f57bc11190ffb1c9'
+ 'be52026eee470d70dcbf3ab364ef8ec92155b7990f589526928d46873cc72cdf'
+ 'd2d69b2bf6315bd37ff5f5b2f0cde8ab2fb89bae18f8796dc5208ffc1a9d743e'
+ 'a745356952470a5d718bef1961309ea30e4fa6a2860961cd52e09d6ec11e708e'
+ '1819085bd2c9106482c5f243b95fddf3dae69212330ab76cb493add5c26a45a4'
+ '62a5a195ddfaa13bbc7b5bc627cdd748f5d697d178a5238b71a703533bfdd587'
+ '8d92160ef094c6186cb0aaeeddcf8730f3e1a05a933ad4147269a74f09c70b19'
+ 'bfb0cc87bf350214f38a262400bee0174fca75f15bed4eec6bdad4a34dde174c'
+ 'bffcc13e4b480f720feb2b3c781bc4247c63303250c3d885022c699573d45a33'
+ '51093acab5e5a4de51a55f4bdf7b935f4f69edf3d84f1c37db710853ec95eca8'
+ 'ad4ddabbc34d5e1b308ece33cabf91d750f44894c52a18325762dea026152973'
+ 'a961cfdc6f02b12fb445777dd2c144fed96306ca2f430cc8853ae307c759c1ad'
+ 'd8574104b75c6d41284488612ec5583c50a8dab438492fa42c47231add4cfc54'
+ '6bdbf593d3e1348d1a8f7c4c17cb2e893f7e18ae355daf978173e669cfe3be80'
+ 'b3f02ec3f8cedc98c74b3169049b7b6aa78bd79d558a8bc98d00e064b983965c'
+ '186203d3c0520bb3d611da99d33a7713e9c1563814285f1f101097234f214b2f'
+ 'd8d36a501928d0cc505957d392291fad317b1e895ff99847d90643cf5f622a89'
+ 'cdfc0c836b8194f631f4a9e022c232ff75a13ff1a161a1a011858578bea5f930'
+ '5f502b1bc8815d69c802320790745e4526d5817fd8ecc7b00cf8b16078f8d440'
+ '12e424432bdf2d50afe3e632c018fef847e860a35a53525eccbe656b9c4118aa'
+ 'ee745056d018f860572f8363ed5e730ba501394c23cdd6f316719d7141c10050'
+ '77fc99fee33387a91e158c0a4e6f3d99601e4d27d04e26d3f815634f48de6a79'
+ '0fbc05f521aea83157c5e6f8bd29a422873093bb6cded965cb7ffe98ff776fa4'
+ '4c29e0b71071ae9556cf2dbd75de560d028577fe5eb993113105112c4b445eac'
+ '890482242434e333024c7819e8bf3c889dc16548d0a1745479c8523930fb32f7'
+ '71c564a12d9a8e2814a2bf67a1a3d70c1e9d3b50bc108f7043ed8c958c067b01'
+ '6c42b2920c6a37bf3dd05755b9e3fdd80137708cc55a7d1bef2234c17dff0349'
+ '09b9c1b2ee6a5bbe48c5f3755ad64628487b60a4eb6734efb292cefdb74fb657'
+ 'efc2a04b67ea76661157e154228d4b58ae2e1b652f8ce41bc001a5a863d13573'
+ '67d0cb111d47609de4c60b84dc617fc817fdf092763fbd0cef270b5ee650d702'
+ '023cba18580819018413608ec055d5ff8166b69525c32aa728ad1ecd7cf00aca'
+ 'dae799f09a076e0f573ea516bc357f56f2fd3aa8e35ec0cf54b6b62282960ee0'
+ 'aa1d831f25317a9cb8d7e9a7d7b6d51c8d03bd5b69f3bec27bea7644ca38fe9a'
+ 'cf884bad5caab9d5901b88b0ef41e3a39ea0b7a4614b8d14707d79941c899cbf'
+ '3ed91fb5a7894f82fb4895e06d2e3f1df3ac4f82e46c970d4a85aaa4edc24cf1'
+ '197ff4bb3c1cafcb197268cac335f1f75ae26873aca5833d62cc51fade85176c'
+ '0b9868a563036c81f8fdb8ab8bbad51934aca2a07e9d7634e24214791afda8e2'
+ '1b965b5e7eeafdf3815c8f2722587a560693dd780327cca9910dc47fba0f1aef'
+ 'd09244af78e693cf0eaa14b7bd0d7535cb8f6c0a78eb0e1f0a0fd2bfd5ec56c1'
+ '2b91f6eb8c010a0dce1f41149c0549d067915fba93251c7af7e5328a05977f0c')
+
+package() {
+ cd $srcdir
+
+ install -m755 -D linhes-profile.sh $pkgdir/etc/profile.d/linhes-profile.sh
+
+ #bin files
+ BINDIR=$pkgdir/usr/bin
+ for i in $binfiles
+ do
+ item=$i
+ echo "installing $item to $BINDIR"
+ install -m755 -D $item $BINDIR/$item
+ done
+
+ #release files
+ install -Dm644 "$srcdir/LinHES-release" "$pkgdir/etc/LinHES-release"
+
+ #startup files
+ install -Dm644 "$srcdir/lh_system_start.sh.desktop" "$pkgdir/etc/skel/.config/autostart/lh_system_start.sh.desktop"
+
+ #systemd files
+ install -Dm644 $srcdir/x11vnc.override.conf $pkgdir/etc/systemd/system/x11vnc.service.d/override.conf
+
+ #sql files
+ install -Dm644 $srcdir/lh_sqlserver.cnf $pkgdir/etc/my.cnf.d/lh_sqlserver.cnf
+
+ #php files
+ install -Dm644 $srcdir/lh_php.ini $pkgdir/etc/php/conf.d/lh_php.ini
+
+ #lighttpd files
+ install -Dm644 $srcdir/lh_lighttpd.conf $pkgdir/etc/lighttpd/conf.d/lh_lighttpd.conf
+
+ #gen_lib_xml.py files
+ install -Dm644 ${srcdir}/plex_lib.conf ${pkgdir}/etc/gen_lib_xml.d/plex_lib.conf
+
+ #rsyslog files
+ install -Dm644 $srcdir/rsyslog.mythtv.conf $pkgdir/etc/rsyslog.d/mythtv.conf
+
+ #remote files
+ install -Dm644 $srcdir/rc6_mce.toml $pkgdir/etc/rc_keymaps/rc6_mce.toml
+
+ #readme files
+ install -Dm644 $srcdir/readme_is_xml $pkgdir/etc/gen_is_xml.d/readme_is_xml
+ install -Dm644 $srcdir/readme_is_xml $pkgdir/etc/gen_lib_xml.d/readme_gen_xml
+ install -Dm644 $srcdir/readme_is_xml $pkgdir/etc/gen_game_xml.d/readme_gen_xml
+ install -Dm644 $srcdir/add_storage.readme $pkgdir/etc/storage.d/readme
+
+ #cron files
+ install -Dm755 $srcdir/paccache.cron $pkgdir/etc/cron.weekly/paccache
+ install -Dm755 $srcdir/xfs_defrag.cron $pkgdir/etc/cron.weekly/xfs_defrag
+ install -Dm755 $srcdir/lh_mtc.cron $pkgdir/etc/cron.hourly/lh_mtc
+ install -Dm755 $srcdir/flatpak_update.cron $pkgdir/etc/cron.daily/flatpak_update
+ install -Dm755 $srcdir/lh_log_care.cron $pkgdir/etc/cron.daily/lh_log_care
+ install -Dm755 $srcdir/recordings.cron $pkgdir/etc/cron.hourly/recordings
+
+ #hooks
+ install -Dm0644 $srcdir/79-cronie.hook "${pkgdir}"/usr/share/libalpm/hooks/79-cronie.hook
+ install -Dm0644 $srcdir/fstrim.hook "${pkgdir}"/usr/share/libalpm/hooks/fstrim.hook
+ install -Dm0644 $srcdir/openssh.hook "${pkgdir}"/usr/share/libalpm/hooks/openssh.hook
+ install -Dm0644 $srcdir/lh_restart_needed.hook "${pkgdir}"/usr/share/libalpm/hooks/lh_restart_needed.hook
+ install -Dm0644 $srcdir/rsyslog.hook "${pkgdir}"/usr/share/libalpm/hooks/rsyslog.hook
+
+ #sudo rules
+ install -Dm0750 "$srcdir/system-sudo.rules" "$pkgdir/etc/sudoers.d/system_sudo"
+ chmod 750 $pkgdir/etc/sudoers.d/
+
+ #udev rules
+ install -Dm0644 "$srcdir/81-wol.rules" "$pkgdir/etc/udev/rules.d/81-wol.rules"
+
+ #disable dpms
+ install -Dm0644 "$srcdir/10-monitor.conf" "$pkgdir/etc/X11/xorg.conf.d/10-monitor.conf"
+}
diff --git a/linhes/linhes-system/add_storage.py b/linhes/linhes-system/add_storage.py
new file mode 100755
index 0000000..cc6feb5
--- /dev/null
+++ b/linhes/linhes-system/add_storage.py
@@ -0,0 +1,1120 @@
+#!/usr/bin/python
+#add_storage.py used to auto add new storage to MythTV storage groups
+#If it's a new disk it will erase the entire disk and reformat.
+#
+#Disks that are mounted, in fstab, size < 5000 bytes, optical or
+#have already been seen will not be presented as an option.
+#
+# Version 2.0.2
+
+import dbus
+import pickle
+import subprocess
+import sys,os,re
+import random,string
+import configparser
+from configparser import ConfigParser
+import glob
+import logging
+from MythTV import MythDB, MythBE, Recorded, MythError
+from socket import timeout, gethostname
+
+
+storage_dir = "/etc/storage.d"
+pickle_file = "%s/storage.pkl" %storage_dir
+
+
+SG_MAP={
+ 'Default' :'media/tv/',
+ 'LiveTV' :'media/tv/live/',
+ 'DB Backups' :'backup/mythtv_backups/',
+ 'Music' :'media/music/',
+ 'Streaming' :'media/streaming/',
+ 'Videos' :'media/video/',
+ 'Photographs':'media/photos/',
+ 'Banners' :'media/artwork/banners/',
+ 'Coverart' :'media/artwork/coverart/',
+ 'Fanart' :'media/artwork/fanart/',
+ 'MusicArt' :'media/artwork/musicart/',
+ 'Screenshots':'media/artwork/screenshots/',
+ 'Trailers' :'media/artwork/trailers/',
+ }
+
+FS_LIST=[]
+for key in list(SG_MAP.keys()):
+ FS_LIST.append(SG_MAP[key])
+
+class disk_device:
+ def __init__(self,device,storage_dir):
+ block_dev = bus.get_object("org.freedesktop.UDisks2", device)
+
+ self.block_path = block_dev.Get('org.freedesktop.UDisks2.Block', 'Device', dbus_interface='org.freedesktop.DBus.Properties')
+ self.block_path = bytearray(self.block_path).replace(b'\x00', b'').decode('utf-8')
+ logging.info("Device: %s", self.block_path)
+ self.read_only = self.get_is_readonly(block_dev)
+ logging.info("ReadOnly: %s", self.read_only)
+ self.device_file_path = self.get_device_file_path(block_dev)
+ logging.info("Device File Path: %s", self.device_file_path)
+ self.device_id = block_dev.Get('org.freedesktop.UDisks2.Block', 'Id', dbus_interface='org.freedesktop.DBus.Properties')
+ logging.info("Device Id: %s", self.device_id)
+ self.is_device = self.get_is_device()
+ logging.info("Is Device: %s", self.is_device)
+
+ self.drive = block_dev.Get('org.freedesktop.UDisks2.Block', 'Drive', dbus_interface='org.freedesktop.DBus.Properties')
+ logging.info("Drive: %s", self.drive)
+ drive_dev = bus.get_object("org.freedesktop.UDisks2", self.drive)
+
+ self.storage_dir = storage_dir
+ self.top_mount_dir = "/data/storage"
+ self.config = configparser.RawConfigParser()
+ self.fs_map = self.get_fsmap()
+
+ self.vendor = drive_dev.Get('org.freedesktop.UDisks2.Drive', 'Vendor', dbus_interface='org.freedesktop.DBus.Properties')
+ logging.info("Vendor: %s", self.vendor)
+ self.model = drive_dev.Get('org.freedesktop.UDisks2.Drive', 'Model', dbus_interface='org.freedesktop.DBus.Properties')
+ logging.info("Model: %s", self.model)
+ self.device_size = drive_dev.Get('org.freedesktop.UDisks2.Drive', 'Size', dbus_interface='org.freedesktop.DBus.Properties')
+ logging.info("Drive Size: %s", self.device_size)
+ self.serial_number = self.get_serial_number(drive_dev)
+ logging.info("Serial: %s", self.serial_number)
+ self.is_optical = self.get_is_optical_disc(drive_dev)
+ logging.info("Is Optical: %s", self.is_optical)
+
+ self.mmount = False
+ self.dir_sg = False
+
+ try:
+ self.f = block_dev.Get('org.freedesktop.UDisks2.Filesystem', 'MountPoints', dbus_interface='org.freedesktop.DBus.Properties')
+ self.is_mounted = True
+ self.f[0] = bytearray(self.f[0]).replace(b'\x00', b'').decode('utf-8')
+ logging.info("MountPoints: %s", self.f[0])
+ except:
+ self.f = ['']
+ self.is_mounted = False
+ logging.info("MountPoints: %s", self.f[0])
+ logging.info("Is Mounted: %s", self.is_mounted)
+
+ try:
+ self.partition_size = block_dev.Get('org.freedesktop.UDisks2.Partition', 'Size', dbus_interface='org.freedesktop.DBus.Properties')
+ except:
+ self.partition_size = 0
+ logging.info("Partition Size: %s", self.partition_size)
+
+ self.set_partition("1")
+ logging.info("Block Partition: %s", self.block_partition)
+
+ self.in_use = self.get_in_use()
+ logging.info("In Use: %s", self.in_use)
+ self.uuid=''
+ self.new_mount_point=''
+ self.disk_num=''
+
+ def set_partition(self,partition):
+ if self.is_device:
+ if 'nvme' in self.block_path:
+ self.block_partition = "%sp%s" %(self.block_path,partition)
+ else:
+ self.block_partition = "%s%s" %(self.block_path,partition)
+ else:
+ self.block_partition = self.block_path
+
+ def set_mmount(self,flag):
+ self.mmount = flag
+
+ def set_dir_sg(self,flag):
+ self.dir_sg = flag
+
+ def set_disk_num(self,num):
+ self.disk_num=num
+
+ def get_name(self):
+ filename="%s_%s" %(self.model.replace(' ',''),
+ self.serial_number.replace(' ',''))
+ return filename
+
+ def get_is_readonly(self,block_dev):
+ readonly = block_dev.Get('org.freedesktop.UDisks2.Block', 'ReadOnly', dbus_interface='org.freedesktop.DBus.Properties')
+ if readonly == 0:
+ return False
+ else:
+ return True
+
+ def get_is_optical_disc(self,drive_dev):
+ optical = drive_dev.Get('org.freedesktop.UDisks2.Drive', 'Optical', dbus_interface='org.freedesktop.DBus.Properties')
+ if optical == 0:
+ return False
+ else:
+ return True
+
+ def get_is_device(self):
+ match = re.search(r'part\d+$', self.device_file_path)
+ if match is None:
+ return True
+ else:
+ return False
+
+ def get_in_use(self):
+ in_use = False
+ for i in self.fs_map:
+ if self.block_path in i[0]:
+ in_use = True
+ break
+ return in_use
+
+ def get_serial_number(self,drive_dev):
+ serial_number = drive_dev.Get('org.freedesktop.UDisks2.Drive', 'Serial', dbus_interface='org.freedesktop.DBus.Properties')
+ random_string = os.urandom(5)
+ if serial_number == '':
+ serial_number = "".join( [random.choice(string.ascii_letters) for i in range(6)] )
+ serial_number = "NSW%s" %serial_number
+ return serial_number
+
+ def get_device_file_path(self,block_dev):
+ paths = block_dev.Get('org.freedesktop.UDisks2.Block', 'Symlinks', dbus_interface='org.freedesktop.DBus.Properties')
+ try:
+ for path in paths:
+ path = bytearray(path).replace(b'\x00', b'').decode('utf-8')
+ if path.startswith('/dev/disk/by-path'):
+ return path
+ except:
+ path = "None"
+
+ def partition_disk(self):
+ print(" Creating new partition table")
+ cmd = "parted -s -a optimal %s mklabel gpt" %self.block_path
+ runcmd(cmd)
+ cmd = "parted -s -a optimal %s mkpart primary \" 1 -1\"" %self.block_path
+ runcmd(cmd)
+ return
+
+ def get_fsmap(self):
+ fs_map=[]
+
+ f = open('/proc/mounts','r')
+ mounts=f.readlines()
+ f.close()
+ for line in mounts:
+ temp_fs=[]
+ split_line=line.split()
+ block=split_line[0]
+ mountp=split_line[1]
+ fs=split_line[2]
+ block=os.path.realpath(block)
+ if block.startswith("/dev"):
+ temp_fs.append(block)
+ temp_fs.append(mountp)
+ temp_fs.append(fs)
+ fs_map.append(temp_fs)
+ return fs_map
+
+ def find_fstype(self,moutpoint):
+ fstype="xfs"
+ mp=['/myth', '/data/storage/disk0']
+ for i in self.fs_map:
+ if i[1] in mp:
+ fstype = i[2]
+ break
+ return fstype
+
+ def lookup_format(self):
+ fstab = self.read_fstab()
+ current_media_mount = self.find_options_type(fstab)[1]
+ new_fstype = self.find_fstype(current_media_mount)
+ #setting self.new_fstype so that it can be referenced when setting fstab
+ self.new_fstype = new_fstype
+ return
+
+ def format_disk(self):
+ #lookup format
+ #self.lookup_format()
+ #do format
+ if self.new_fstype == "xfs":
+ cmd = "mkfs -t %s -f %s " %(self.new_fstype,self.block_partition)
+ else:
+ cmd = "mkfs -t %s %s " %(self.new_fstype,self.block_partition)
+ print(" Formatting %s with %s" %(self.block_partition,self.new_fstype))
+ runcmd(cmd)
+ return
+
+ def find_uuid(self,partition):
+ #logging.info("Finding the UUID for %s...", partition)
+ cmd = "blkid -s UUID %s" %partition
+ tmpuuid = runcmd(cmd)[1]
+ splituuid = tmpuuid.partition("=")
+ uuid = splituuid[2].replace('"', "")
+ #logging.info("The uuid is %s", uuid)
+ if uuid == '':
+ print("Could not find a UUID for device: %s" %partition)
+ sys.exit(1)
+ self.uuid = uuid.strip()
+ return uuid.strip()
+
+ def read_fstab(self):
+ f = open('/etc/fstab', 'r')
+ fstab=f.readlines()
+ f.close()
+ return fstab
+
+ def find_options_type(self,fstab):
+ mp=['/myth', '/data/storage/disk0']
+ for i in fstab:
+ split_line=i.split()
+ try:
+ if split_line[1] in mp:
+ options = split_line[3]
+ break
+ else:
+ options = "defaults"
+ mount_point = i
+ except:
+ options = "defaults"
+ return options,i
+
+
+ def add_fstab(self):
+ #new_fstab_list=['UUID=', 'mount_point', 'auto', 'defaults', '0', '1']
+ new_fstab_list=['UUID=', 'mount_point', self.new_fstype, 'defaults', '0', '1']
+ fstab=self.read_fstab()
+ new_fstab=[]
+
+ #determine mount_path
+ self.new_mount_point="%s/%s_%s" %(self.top_mount_dir,self.model.replace(' ',''),self.serial_number.replace(' ',''))
+
+ #check for old mount point and comment out
+ for line in fstab:
+ if not line.startswith("#"):
+ if line.find(self.new_mount_point) > -1:
+ print(" Found old mount %s in fstab, commenting out" %self.new_mount_point)
+ line = "#"+line
+ new_fstab.append(line)
+ fstab=new_fstab
+
+ #determine options
+ #new_options = self.find_options_type(fstab)[0]
+ new_options = "nofail,x-systemd.device-timeout=10"
+
+ #find blkid
+ self.block_partition="%s1" %self.block_path
+ uuid=self.find_uuid(self.block_partition)
+
+ #construct new line
+ new_fstab_list[0]="UUID=%s" %uuid
+ new_fstab_list[1]=self.new_mount_point
+ new_fstab_list[3]=new_options
+ if self.new_fstype == "xfs":
+ new_fstab_list[5]="0"
+
+ new_fstab_line='\t'.join(new_fstab_list)
+ new_fstab_line="%s\n" %new_fstab_line
+ fstab.append(new_fstab_line)
+
+ #add line to fstab
+ f = open('/etc/fstab', 'w')
+ for i in fstab:
+ f.write(i)
+ f.close()
+ return
+
+ def mount_disk(self,no_mount=False):
+ try:
+ os.stat(self.new_mount_point)
+ except:
+ os.makedirs(self.new_mount_point)
+ if no_mount == False:
+ if os.path.ismount(self.new_mount_point):
+ print(" Disk already mounted, will not mount:\n %s" %self.new_mount_point)
+ pass
+ else:
+ print(" Mounting %s" %self.new_mount_point)
+ cmd = "mount %s" %self.new_mount_point
+ runcmd(cmd)
+ return
+
+ def mkdirs(self,FS_LIST):
+ print(" Creating directory structure:")
+ print(" %s" %self.new_mount_point)
+ for y in FS_LIST:
+ print(" %s" %y)
+ new_dir="%s/%s" %(self.new_mount_point,y)
+ try:
+ os.stat(new_dir)
+ except:
+ os.makedirs(new_dir)
+ cmd="chown -R mythtv:mythtv /%s" %self.new_mount_point
+ runcmd(cmd)
+ cmd="chmod -R 775 /%s" %self.new_mount_point
+ runcmd(cmd)
+
+ def add_sg(self,DB,host,SG_MAP,weight='0',install_call=False):
+ print(" Adding storage groups")
+ sgweight=int(weight)
+ for key in SG_MAP.keys():
+ #print key," : ", SG_MAP[key]
+ gn=key
+ hn=host
+ dn="%s/%s" %(self.new_mount_point,SG_MAP[key])
+ #print dn
+ #print gn
+ #print hn
+ if install_call == True :
+ print("Will write SG for stuff after the fact")
+ else:
+ with DB as c:
+ #delete old dir without trailing slash
+ c.execute("""delete from storagegroup where groupname = %s and hostname = %s and dirname = %s""", (gn,hn,dn.rstrip('/')))
+
+ try:
+ c.execute("""insert into storagegroup (groupname,hostname,dirname) values (%s,%s,%s)""",(gn,hn,dn))
+ print(" Adding location: %s to storagegroup %s" %(dn,gn))
+ except:
+ print(" *Error inserting %s into storage groups" %dn)
+
+ if sgweight > 0:
+ try:
+ #("SGweightPerDir:server2:/mnt/video", 99, "server2");
+ sgw="SGweightPerDir:%s:%s" %(hn,dn)
+ #print sgw
+ #print sgweight
+ #print hn
+
+ #delete old dir without trailing slash
+ c.execute("""delete from settings where value = %s and data = %s and hostname = %s""", (sgw.rstrip('/'),sgweight,hn))
+
+ c.execute("""insert into settings (value,data,hostname) values (%s,%s,%s)""",(sgw,sgweight,hn))
+ print(" Adding storage group weight of %s for %s\n" %(sgweight,gn))
+ except:
+ print(" *Error setting storage group weight %s for %s\n" %(sgweight,gn))
+
+ return
+
+ def write_config(self):
+ print(" Writing /etc/storage.d conf file")
+ self.config.add_section('storage')
+ self.config.set('storage','uuid',self.uuid)
+ self.config.set('storage','mountpoint',self.new_mount_point)
+ self.config.set('storage','fstype',self.new_fstype)
+ self.config.set('storage','shareable','True')
+ self.config.set('storage','mmount',self.mmount)
+ self.config.set('storage','storage_groups',self.dir_sg)
+ self.config.set('storage','disk_num',self.disk_num)
+
+ filename="%s_%s.conf" %(self.model.replace(' ',''),
+ self.serial_number.replace(' ',''))
+
+ configfile="/etc/storage.d/%s" %filename
+ print(" %s" %configfile)
+ with open(configfile, 'w') as configfile:
+ self.config.write(configfile)
+ return
+
+ def symlink_disk(self):
+ print(" Creating symlink for disk%s" %self.disk_num)
+ disk_ln="%s/disk%s" %(self.top_mount_dir,self.disk_num)
+ cmd = "ln -s %s %s" %(self.new_mount_point,disk_ln)
+ runcmd(cmd)
+
+
+#end of class
+
+
+def runcmd(cmd):
+ if True :
+ pass
+ else:
+ cmd = "echo "+cmd
+ #print(cmd)
+ cmdout = subprocess.getstatusoutput(cmd)
+ #logging.debug(" %s", cmdout)
+ return cmdout
+
+
+
+
+def scan_system():
+ ud_manager_obj = bus.get_object("org.freedesktop.UDisks2", "/org/freedesktop/UDisks2")
+ ud_manager = dbus.Interface(ud_manager_obj, 'org.freedesktop.DBus.ObjectManager')
+ current_drive_list=[]
+ for dev in ud_manager.GetManagedObjects().items():
+ if dev[0].startswith("/org/freedesktop/UDisks2/block_devices"):
+ logging.info(dev[0])
+ drive = disk_device(dev[0],storage_dir)
+ if drive.is_device and drive.device_size > 5000 and not drive.is_optical :
+ current_drive_list.append(drive)
+ return current_drive_list
+
+def read_known_list():
+ #reading pickle file
+ known_drive_list=[]
+ try:
+ pkl_file = open(pickle_file, 'rb')
+ known_drive_list = pickle.load(pkl_file)
+ pkl_file.close()
+ except:
+ pass
+ return known_drive_list
+
+
+def write_known_drive_list(known_drive_list):
+ output = open(pickle_file, 'wb')
+ pickle.dump(known_drive_list, output, -1)
+ output.close()
+
+
+def search_for_match(system_drive,known_drive_list):
+ match_drive=False
+ for y in known_drive_list:
+ if system_drive.serial_number.startswith("NSW"):
+ #print("Match_test: hash")
+ system_drive_hash = "%s_%s_%s" %(system_drive.model,system_drive.partition_size,system_drive.device_file_path)
+ y_drive_hash = "%s_%s_%s" %(y.model,y.partition_size,y.device_file_path)
+ if system_drive_hash == y_drive_hash :
+ match_drive = True
+ print("\n* No serial number was found, matched using hash method: %s" %system_drive.model)
+ break
+
+ elif y.serial_number == system_drive.serial_number:
+ #print("Match_test: serial number")
+ match_drive=True
+ break
+
+ return match_drive
+
+
+def prompt_to_add(current_drive,destruction = True):
+ loop = True
+ if destruction :
+ prompt = '''
+ ** Adding this disk will remove all contents on the disk. **
+ This disk will be partitioned and formatted.
+
+ Enable this disk for storage (Y/N)?:'''
+ else:
+ prompt = '''
+ ** Preserving existing contents on the disk. **
+ This disk will NOT be partitioned or formatted.
+
+ Enable this disk for storage (Y/N)?:'''
+ while loop:
+ str1 = input(prompt)
+
+ if str1 in ['Y','N','y','n']:
+ loop = False
+ break
+ print("\n")
+ if str1 == 'Y' or str1 == 'y':
+ rc = True
+ else:
+ rc = False
+ return rc
+
+def prompt_to_continue(process_list):
+ loop = True
+ print("\n\n\n Ready to add additional storage!\n")
+ if destruction:
+ print("** WARNING: These disk(s) WILL be partitioned and formatted. **\n ** All content on these disk(s) will be erased. **")
+ else:
+ print(" ** These disk(s) will NOT be partitioned and formatted. **")
+ for i in process_list:
+ print(" %s" %(i.get_name()))
+ str1 = input("\n Press Y to add disk(s), any other key to cancel:")
+
+ if str1 == 'Y' or str1 == 'y':
+ rc = True
+ else:
+ rc = False
+ print("\nCancelled: No disk(s) added to your system.")
+ print("-----")
+ return rc
+
+def prompt_sg(dir_sg):
+ #check for storage groups
+ print("*" * 60)
+ if dir_sg != True:
+ loop = True
+ prompt_string='''
+ MythTV storage groups are used for artwork, database backups,
+ photos, music, streaming, TV recordings, and videos.
+
+ The content on these storage groups will
+ only be available while the system is online.
+
+ Enabling MythTV storage groups will create the directories
+ on the disk(s) and add the paths to the MythTV database.
+
+ Enable MythTV storage groups (Y/N)?:'''
+
+ while loop:
+ str1 = input(prompt_string)
+ if str1 in ['Y','N','y','n']:
+ loop = False
+ break
+ print("\n")
+
+ if str1 == 'Y' or str1 == 'y':
+ dir_sg = True
+ print(" ** Will add MythTV storage groups!")
+ else:
+ print(" ** Will NOT add MythTV storage groups!")
+ dir_sg = False
+ else:
+ dir_sg = True
+ print("\n --add_sg option used")
+ print(" ** Will add MythTV storage groups!")
+
+ return dir_sg
+
+def remove_pickle():
+ try:
+ print("\n* Removing list of known disks.\n")
+ os.remove(pickle_file)
+ except:
+ pass
+
+def last_disk_num():
+ parser = ConfigParser()
+ num_list = []
+ for conf_file in glob.glob('%s/*.conf' %storage_dir):
+ parser.read(conf_file)
+ try:
+ disk_num = parser.get('storage', 'disk_num')
+ except:
+ print("\nSkipping " + conf_file + "is missing disk_num.")
+ continue
+ num_list.append(int(disk_num))
+ num_list.sort()
+ try:
+ return num_list[-1]
+ except:
+ # conf file or disk_num is missing so fallback to /data/storage/disk# links
+ for disk_name in glob.glob('/data/storage/disk*'):
+ disk_num = disk_name.strip('/data/storage/disk')
+ num_list.append(int(disk_num))
+ num_list.sort()
+ try:
+ return num_list[-1]
+ except:
+ print("Couldn't find last disk number.")
+ sys.exit(1)
+
+#--------------------------------------------
+
+def main(scan_only, destruction, no_mount, install_call, dir_sg):
+ global bus
+ bus = dbus.SystemBus()
+
+ system_drive_list = scan_system()
+ known_drive_list=[]
+ known_drive_list = read_known_list()
+ process_list=[]
+ no_process_list=[]
+
+ print("-" * 60)
+ print(" Scan for Disks")
+
+ for i in system_drive_list:
+ #print i.mount_path
+ #print i.is_mounted
+ #print i.in_use
+ #print i.model
+ #print i.block_path
+ #print "--"
+
+ if search_for_match(i,known_drive_list) or i.in_use :
+ if search_for_match(i,known_drive_list) :
+ dstatus = " Ignoring - Disk has been previously skipped:"
+ if i.in_use :
+ dstatus = " Ignoring - Disk is mounted:"
+ if search_for_match(i,known_drive_list) and i.in_use :
+ dstatus = " Ignoring - Disk has been previously skipped and is mounted:"
+
+ print("\n")
+ print(" --------------------------------------------------------")
+ print(dstatus)
+ print(" model: %s" %i.model)
+ print(" location: %s" %i.block_path)
+ print(" size: %s" %i.device_size)
+ continue
+
+ else:
+ if not scan_only:
+ print("\n")
+ print(" --------------------------------------------------------")
+ print(" Found New Disk:")
+ print(" model: %s" %i.model)
+ print(" location: %s" %i.block_path)
+ print(" size: %s " %i.device_size)
+
+ if prompt_to_add(i,destruction) :
+ print("\n %s will be added to your system!" %i.model)
+ process_list.append(i)
+ else:
+ no_process_list.append(i)
+ else:
+ process_list.append(i)
+ print("\n")
+ print(" Scan Finished")
+ print("-" * 60)
+
+ if scan_only:
+ if len(process_list) > 0:
+ print(" Unknown or Unmounted Disks:")
+ f = open('/tmp/scan_report', 'w')
+ for i in process_list:
+ f.write("disk: %s, location: %s, size: %s\n" %(i.model,i.block_path,i.device_size))
+ print("\n")
+ print(" ---------------------------------------------------------")
+ print(" Found New Disk:")
+ print(" model: %s" %i.model)
+ print(" location: %s" %i.block_path)
+ print(" size: %s " %i.device_size)
+ f.close()
+ sys.exit(0)
+
+ for i in no_process_list:
+ system_drive_list.append(i)
+
+
+ if len(process_list) > 0:
+ #DB = MythDB()
+ host=gethostname()
+ for y in process_list:
+ system_drive_list.remove(y)
+ write_known_drive_list(system_drive_list)
+ else:
+ print("\nThere are no disks to add to your system.\n\nFor more options: add_storage.py --help\n")
+ write_known_drive_list(system_drive_list)
+
+
+ if len(process_list) > 0:
+ print("\n Will add %s disk(s) to your system." %len(process_list))
+
+ dir_sg = prompt_sg(dir_sg)
+ if prompt_to_continue(process_list) == True:
+ write_known_drive_list(system_drive_list)
+ disk_num = last_disk_num()
+ for i in process_list:
+ print(" Disk: %s" %(i.get_name()))
+ disk_num = disk_num + 1
+ i.lookup_format()
+ if destruction == True:
+ i.partition_disk()
+ i.format_disk()
+ i.add_fstab()
+ i.mount_disk(no_mount)
+
+ if dir_sg == True:
+ i.mkdirs(FS_LIST)
+
+ i.set_disk_num(disk_num)
+ i.set_dir_sg(dir_sg)
+ i.write_config()
+ system_drive_list.append(i)
+ write_known_drive_list(system_drive_list)
+
+ i.symlink_disk()
+
+ if dir_sg == True:
+ DB = MythDB()
+ i.add_sg(DB,host,SG_MAP)
+
+ print("-----")
+
+def reconstruct_storagegroups():
+ print("\nRecreating storage groups from contents of /etc/storage.d/\n")
+
+ DB = MythDB()
+ host=gethostname()
+
+ for conf_file in glob.glob('%s/*.conf' %storage_dir):
+ parser = ConfigParser()
+ parser.read(conf_file)
+ try:
+ mount_point = parser.get('storage', 'mountpoint')
+ except:
+ print("\nSkipping: " + conf_file + " is missing mountpoint")
+ continue
+ mmount = parser.getboolean('storage', 'mmount')
+ try:
+ removed = parser.getboolean('storage', 'removed')
+ except:
+ removed = False
+ if removed:
+ print("Skipping: " + mount_point + " - removed")
+ continue
+ if not os.path.ismount(mount_point):
+ print("Skipping: " + mount_point + " - not mounted")
+ continue
+ try:
+ dir_sg = parser.getboolean('storage', 'storage_groups')
+ except configparser.NoOptionError as err:
+ print("SG not found in conf, get setting from DB")
+ dir_sg = False
+ # Get storage group directories from DB
+ recs = DB.getStorageGroup(groupname="Default")
+ for record in recs:
+ if record.dirname.startswith(mount_point):
+ dir_sg = True
+ # Write SG usage to conf
+ parser.set('storage','storage_groups',str(dir_sg))
+ with open(conf_file, 'wb') as conf_file:
+ parser.write(conf_file)
+
+ if dir_sg is True:
+ print("SGs Enabled for: " + mount_point)
+ print(" Creating directory structure:")
+ print(" %s" %mount_point)
+ for y in FS_LIST:
+ new_dir="%s/%s" %(mount_point,y)
+ try:
+ os.stat(new_dir)
+ print(" %s - exists" %y)
+ except:
+ os.makedirs(new_dir)
+ cmd="chown -R mythtv:mythtv /%s" %new_dir
+ runcmd(cmd)
+ cmd="chmod -R 775 /%s" %new_dir
+ runcmd(cmd)
+ print(" %s - created" %y)
+
+ print(" Adding storage groups to DB")
+ if mmount is True:
+ sgweight=99
+ else:
+ sgweight=0
+
+ for key in SG_MAP.keys():
+ gn=key
+ hn=host
+ dn="%s/%s" %(mount_point,SG_MAP[key])
+ with DB as c:
+ #delete old dir without trailing slash
+ c.execute("""delete from storagegroup where groupname = %s and hostname = %s and dirname = %s""", (gn,hn,dn.rstrip('/')))
+
+ try:
+ c.execute("""insert into storagegroup (groupname,hostname,dirname) values (%s,%s,%s)""",(gn,hn,dn))
+ print(" Added: %s to storagegroup %s" %(dn,gn))
+ except:
+ print(" Skipping: %s exists" %dn)
+ if sgweight > 0:
+ try:
+ sgw="SGweightPerDir:%s:%s" %(hn,dn)
+
+ #delete old dir without trailing slash
+ c.execute("""delete from settings where value = %s and data = %s and hostname = %s""", (sgw.rstrip('/'),sgweight,hn))
+
+ if DB.settings[hn][sgw] == '99':
+ print(" Skipping: storage group weight DB entry exists")
+ else:
+ c.execute("""insert into settings (value,data,hostname) values (%s,%s,%s)""",(sgw,sgweight,hn))
+ print(" Adding storage group weight of %s for %s\n" %(sgweight,gn))
+ except:
+ print(" *Error setting storage group weight %s for %s\n" %(sgweight,gn))
+
+ else:
+ print("SGs Disabled for: " + mount_point)
+ return
+
+class reconstruct_path:
+ def __init__(self,conf_file):
+ self.conf_file = conf_file
+ parser = ConfigParser()
+ parser.read(self.conf_file)
+ self.config = configparser.RawConfigParser()
+
+ self.uuid = parser.get('storage', 'uuid')
+ self.mount_point = parser.get('storage', 'mountpoint')
+ self.shareable = parser.get('storage', 'shareable')
+ self.disk_num = parser.get('storage', 'disk_num')
+ self.top_mount_dir = os.path.dirname(self.mount_point)
+ try:
+ self.fstype = parser.get('storage', 'fstype')
+ except:
+ self.fstype = self.get_fstype()
+ try:
+ self.removed = parser.get('storage', 'removed')
+ except:
+ self.removed = False
+
+ def get_fstype(self):
+ cmd = "fsck -N UUID=%s" %self.uuid
+ tmpfstype = runcmd(cmd)
+ tmpfstype = tmpfstype[1].split('/sbin/fsck.')
+ tmpfstype = tmpfstype[1].split(' ')
+ self.fstype = tmpfstype[0]
+ self.write_config()
+ return self.fstype
+
+ def get_conf(self):
+ return self.conf_file
+
+ def get_uuid(self):
+ return self.uuid
+
+ def get_mount_point(self):
+ return self.mount_point
+
+ def get_shareable(self):
+ return self.shareable
+
+ def get_is_myth_mount(self):
+ return self.myth_mount
+
+ def get_disk_num(self):
+ return self.disk_num
+
+ def get_removed(self):
+ return self.removed
+
+ def create_mount_point(self):
+ try:
+ os.stat(self.mount_point)
+ except:
+ os.makedirs(self.mount_point)
+
+ def find_options_type(self,fstab):
+ mp=['/myth', '/data/storage/disk0']
+ for i in fstab:
+ split_line=i.split()
+ try:
+ if split_line[1] in mp:
+ options = split_line[3]
+ break
+ else:
+ options = "defaults"
+ mount_point = i
+ except:
+ options = "defaults"
+ return options,i
+
+ def read_fstab(self):
+ f = open('/etc/fstab', 'r')
+ fstab=f.readlines()
+ f.close()
+ return fstab
+
+ def check_in_fstab(self,fstab,check_path):
+ for line in fstab:
+ if line.find(check_path) > -1:
+ return True
+ return False
+
+ def append_fstab(self,line):
+ new_fstab_line='\t'.join(line)
+ new_fstab_line="%s\n" %new_fstab_line
+
+ f = open('/etc/fstab', 'a')
+ f.write(new_fstab_line)
+ f.close()
+
+ def symlink_disk(self):
+ print(" Creating symlink for disk%s" %self.disk_num)
+ disk_ln="%s/disk%s" %(self.top_mount_dir,self.disk_num)
+ if os.path.islink(disk_ln):
+ print(" Symlink %s exists. Skipping." %disk_ln)
+ else:
+ cmd = "ln -s %s %s" %(self.mount_point,disk_ln)
+ runcmd(cmd)
+
+ def add_fstab(self):
+ #new_fstab_list=['UUID=', 'mount_point', 'auto', 'defaults', '0', '1']
+ new_fstab_list=['UUID=', 'mount_point', self.fstype, 'defaults', '0', '1']
+ fstab=self.read_fstab()
+
+ if self.check_in_fstab(fstab,self.uuid) == True:
+ print(" Found UUID of disk in fstab, will not add it")
+ else:
+ print(" Adding storage to fstab")
+ #check for old mount point and comment out
+ f = open('/etc/fstab', 'w')
+ for line in fstab:
+ if not line.startswith("#"):
+ if line.find(self.mount_point) > -1:
+ print(" Found old mount %s in fstab, commenting out" %self.mount_point)
+ line = "#"+line
+ f.write(line)
+ f.close()
+
+ #construct new line
+ #new_options = self.find_options_type(fstab)[0]
+ new_options = "nofail,x-systemd.device-timeout=10"
+ new_fstab_list[0]="UUID=%s" %self.uuid
+ new_fstab_list[1]=self.mount_point
+ new_fstab_list[3]=new_options
+ if self.fstype == "xfs":
+ new_fstab_list[5]="0"
+ self.append_fstab(new_fstab_list)
+
+ def mount_disk(self,no_mount=False):
+ try:
+ os.stat(self.mount_point)
+ except:
+ os.makedirs(self.mount_point)
+ if no_mount == False :
+ if os.path.ismount(self.mount_point):
+ print(" Disk already mounted, will not mount:\n %s" %self.mount_point)
+ pass
+ else:
+ print(" Mounting %s" %self.mount_point)
+ cmd = "mount %s" %self.mount_point
+ runcmd(cmd)
+ return
+
+ def write_config(self):
+ print(" Writing /etc/storage.d conf file")
+ self.config.add_section('storage')
+ self.config.set('storage','uuid',self.uuid)
+ self.config.set('storage','mountpoint',self.mount_point)
+ self.config.set('storage','fstype',self.fstype)
+ self.config.set('storage','shareable','True')
+ self.config.set('storage','mmount',self.myth_mount)
+ self.config.set('storage','storage_groups',self.dir_sg)
+ self.config.set('storage','disk_num',self.disk_num)
+
+ print(" %s" %self.conf_file)
+ with open(self.conf_file, 'w') as self.conf_file:
+ self.config.write(self.conf_file)
+ return
+
+def reconstruct_mounts(no_mount):
+ print("\nRecreating disks from contents of /etc/storage.d/")
+ for conf_file in glob.glob('%s/*.conf' %storage_dir):
+ print("\n")
+ cf = reconstruct_path(conf_file)
+
+ # skip if the disk was removed
+ if cf.get_removed():
+ continue
+ #print cf.get_conf()
+ #print cf.get_uuid()
+ print(" Recreating %s" %cf.get_mount_point())
+ #print cf.get_shareable()
+ #print cf.get_is_myth_mount()
+ #print cf.get_disk_num()
+
+ cf.create_mount_point()
+ cf.add_fstab()
+ cf.symlink_disk()
+ cf.mount_disk(no_mount)
+
+ print("\n\nDone recreating disks.\n")
+ pass
+
+
+def usage():
+ help='''
+ add_storage.py finds and sets up disks for MythTV usage.
+ It's a powerful tool that could destroy data if not used correctly,
+ please be careful.
+
+ Scanned disks are ignored if they are mounted or have been
+ previously skipped by add_storage.py.
+
+ The file system type for disks added by add_storage.py is
+ automatically set to the type selected for the data partition
+ at install.
+
+ Normal operations without options include (in this order):
+ Partition the disk
+ Format the disk
+ Add disk to /etc/fstab
+ Mount the disk
+ Create the directories
+ (if user enables MythTV storage groups)
+ Write out the disk config file to /etc/storage.d/
+ Create disk# symlink at /data/storage/
+ Create MythTV storage group paths in MythTV database
+ (if user enables MythTV storage groups)
+
+ Options:
+ --add_sg: Create the MythTV storage group directories and
+ database entries for database backups, TV
+ recordings, photos, music, streaming, videos
+ and artwork.
+ -h, --help: Show this help message.
+ --new_init: Erase the list of known disks and rescan.
+ --no_destruction: Will not partition or format the disk.
+ All other normal operations will be performed.
+ Can be used to import disks from other systems
+ however, add_storage.py only works with the first
+ partition on a disk and ignores all others.
+ --no_mount: Do not mount the disk.
+ All other normal operations will be performed.
+ --reconstruct: Recreate mount point, fstab entry,
+ /data/storage/disk# symlink, and mount the disk.
+ --no_mount is the only option that works with
+ --reconstruct.
+ --reconstruct_sg: Recreate the MythTV storage group directories and
+ database entries if they don't exist.
+ No other options work with --reconstruct_sg.
+ --report: Scan disks and print new found disks.
+ '''
+ print(help)
+ sys.exit(0)
+
+
+
+
+if __name__ == "__main__":
+ scan_only = False
+ myth_mount = False
+ no_mount = False
+ destruction = True
+ install_call = False
+ dir_sg = False
+ reconstruct = False
+ reconstruct_sg = False
+ try:
+ os.remove("/tmp/scan_report")
+ except:
+ pass
+
+ if not os.geteuid()==0:
+ sys.exit("\nRoot access is required to run this program.\n")
+
+ logging.basicConfig(filename='/var/log/add_storage.log', filemode='w',
+ format='%(asctime)s - %(levelname)s - %(message)s',
+ datefmt='%y-%m-%d %H:%M:%S')
+ logger = logging.getLogger()
+ logger.setLevel(logging.INFO)
+
+ if "--help" in sys.argv or "-h" in sys.argv:
+ usage()
+
+ if "--install_call" in sys.argv:
+ install_call = True
+
+ if "--no_mount" in sys.argv :
+ no_mount = True
+
+ if "--no_destruction" in sys.argv:
+ destruction = False
+
+ if "--new_init" in sys.argv :
+ remove_pickle()
+
+ if "--report" in sys.argv :
+ scan_only = True
+
+ if "--add_sg" in sys.argv:
+ dir_sg = True
+
+ #there is no distinction between FE and BE sg anymore
+ #but leaving these for backwards compatibility
+ if "--add_fe_sg" in sys.argv:
+ dir_sg = True
+
+ if "--add_be_sg" in sys.argv:
+ dir_sg = True
+
+ if "--reconstruct" in sys.argv:
+ reconstruct = True
+
+ if "--reconstruct_sg" in sys.argv:
+ reconstruct_sg = True
+
+ if reconstruct == True:
+ reconstruct_mounts(no_mount)
+ elif reconstruct_sg == True:
+ reconstruct_storagegroups()
+ else:
+ main(scan_only, destruction, no_mount, install_call, dir_sg)
diff --git a/linhes/linhes-system/add_storage.readme b/linhes/linhes-system/add_storage.readme
new file mode 100644
index 0000000..d4435d5
--- /dev/null
+++ b/linhes/linhes-system/add_storage.readme
@@ -0,0 +1,3 @@
+Files here are autogenerated by add_storage.py.
+They will be used by systemconfig to generate nfs, smb and mountpoint recovery.
+
diff --git a/linhes/linhes-system/balance_storage_groups.py b/linhes/linhes-system/balance_storage_groups.py
new file mode 100755
index 0000000..d91da60
--- /dev/null
+++ b/linhes/linhes-system/balance_storage_groups.py
@@ -0,0 +1,165 @@
+#!/usr/bin/python
+
+import argparse, glob, operator, os, random, shutil, subprocess, sys, signal, time
+shouldQuit = False
+movingFiles = False
+
+def getFreeSpaceForDir(dir):
+ stats = os.statvfs(dir)
+ return (stats.f_bavail * stats.f_frsize)
+
+def getFreePercentForDir(dir):
+ stats = os.statvfs(dir)
+ total = (stats.f_blocks)
+ avail = (stats.f_bavail)
+ return (total - avail) / float(total)
+
+def getFileSize(fullPath):
+ return os.path.getsize(fullPath)
+
+def sizeof_fmt(num, suffix='B'):
+ for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
+ if abs(num) < 1024.0:
+ return "%3.1f %s%s" % (num, unit, suffix)
+ num /= 1024.0
+ return "%.1f %s%s" % (num, 'Yi', suffix)
+
+def signal_handler(signal, frame):
+ if movingFiles:
+ print("\nWill quit when file has been moved.\nMoving File...")
+ global shouldQuit
+ shouldQuit = True
+ else:
+ sys.exit(0)
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c', '--checkonly', action='store_true', help="Check only, don't move any files.")
+ parser.add_argument('-p', '--percent', type=int, default=7, help="The percentage difference between the most full dir and least full dir that will stop balancing.")
+ cmdargs = parser.parse_args()
+
+ SGDIRS = []
+ SGgrp = "Default"
+
+ signal.signal(signal.SIGINT, signal_handler)
+
+ print("\nBalance MythTV Storage Group Directories\nPress Ctrl+C to quit")
+
+ # Get Storage Groups from MythDB
+ try:
+ from MythTV import MythDB
+ mythDB = MythDB()
+ records = mythDB.getStorageGroup()
+ except:
+ print("Couldn't connect to MythTV database.")
+ sys.exit(1)
+
+ # Get Storage Group directories
+ for record in records:
+ if record.groupname == SGgrp:
+ dirname = record.dirname
+ SGDIRS.append(dirname)
+
+ # If there are less than 2 directories defined bail as we can't move anything
+ if len(SGDIRS) < 2:
+ print("There are less than 2 directories defined. Exiting.")
+ sys.exit(0)
+
+ while not shouldQuit:
+ SGDIRSdata = []
+ print("\n------------------------------------------------")
+ print("'" + SGgrp + "' Storage Group Directories - Percent Used:")
+ # Get percent free and size free
+ for directory in SGDIRS:
+ # Check if SG path exists
+ if not os.path.exists(directory):
+ print(" " + directory + " - Not Mounted")
+ continue
+ freePcent = getFreePercentForDir(directory)
+ freeSize = getFreeSpaceForDir(directory)
+ SGDIRSdata.append([directory, freePcent, freeSize])
+ print(" %s - %.2f%%" % (directory, freePcent * 100))
+
+ # Sort data on percent free
+ SGDIRSdata = sorted(SGDIRSdata, reverse=True, key=operator.itemgetter(1))
+ #print SGDIRSdata
+
+ # Check if SG has any ts, mpg or nuv files
+ i=0
+ for dir in SGDIRSdata:
+ mostFull = SGDIRSdata[i]
+ i=i+1
+ if len(glob.glob1(mostFull[0],"*.ts")) or len(glob.glob1(mostFull[0],"*.mpg")) or len(glob.glob1(mostFull[0],"*.nuv")):
+ break
+ else:
+ if i == 1:
+ print("------------------------------------------------")
+ print(" " + mostFull[0] + " - NO files to move")
+
+ leastFull = SGDIRSdata[-1]
+
+ print("------------------------------------------------")
+ print("Most Used Storage Group Directory with files to move: ")
+ print(" %s - %.2f%%" % (mostFull[0], mostFull[1] * 100))
+ print("Least Used Storage Group Directory: ")
+ print(" %s - %.2f%%" % (leastFull[0], leastFull[1] * 100))
+
+ # Check if mostFull and leastFull are within the percent var of each other
+ if mostFull[1] - (float(cmdargs.percent) / 100) < leastFull[1]:
+ print("\nThe most used and least used storage group directories are\nwithin " + str(cmdargs.percent) + "% used of each other. No files will be moved.")
+ sys.exit()
+
+ # Get random file from most used dir
+ fileToMove = random.choice([f for f in os.listdir(mostFull[0]) if f.endswith(".ts") or f.endswith(".mpg") or f.endswith(".nuv")])
+ filePathToMove = mostFull[0] + "/" + fileToMove
+
+ # Check that the file isn't too big for least used dir
+ fileSize = getFileSize(filePathToMove)
+ if (fileSize > getFreeSpaceForDir(leastFull[0])):
+ # Too big to move
+ print(filePathToMove + " is too big to move to " + leastFull[0])
+ sys.exit()
+
+ # Move file
+ if cmdargs.checkonly:
+ print("------------------------------------------------")
+ print("Check Only option was used. No files were moved.")
+ shouldQuit = True
+ else:
+ isBusy = True
+ while isBusy:
+ if shouldQuit:
+ sys.exit(0)
+ print("------------------------------------------------")
+ print("Checking System Status...")
+ if subprocess.call(["/usr/bin/python", "/usr/bin/idle.py", "-s"]):
+ print(" System is busy. The file will not be moved.")
+ print(" Waiting 5 minutes before trying again.")
+ time.sleep(300)
+ else:
+ isBusy = False
+ print("------------------------------------------------")
+ print("Move File:")
+ print(" " + filePathToMove)
+ print(" Size: " + sizeof_fmt(os.path.getsize(filePathToMove)))
+ print("To:")
+ print(" " + leastFull[0])
+ print(" Available: " + sizeof_fmt(getFreeSpaceForDir(leastFull[0])))
+ print("Moving File...")
+ movingFiles = True
+ try:
+ shutil.move(filePathToMove, leastFull[0])
+ # eg. src and dest are the same file
+ except shutil.Error as e:
+ print(('Error: %s' % e))
+ except IOError as e:
+ print(('Error: %s' % e.strerror))
+
+ # Remove png files
+ print("------------------------------------------------")
+ print("Removing png Files:")
+ pngFiles = glob.glob(filePathToMove + "*.png")
+ for p in pngFiles:
+ os.remove(p)
+ print(" " + p)
+ movingFiles = False
diff --git a/linhes/linhes-system/be_check.py b/linhes/linhes-system/be_check.py
new file mode 100755
index 0000000..1a8dc58
--- /dev/null
+++ b/linhes/linhes-system/be_check.py
@@ -0,0 +1,12 @@
+#!/usr/bin/python
+#simple program to check if mythbackend is up and running
+#exit code of 0 is success, anything else means it can't connect
+import sys
+from MythTV import MythBE,MythDB
+
+try:
+ be = MythBE()
+ db = MythDB()
+except:
+ sys.exit(1)
+sys.exit(0)
diff --git a/linhes/linhes-system/checkXFSfrag.sh b/linhes/linhes-system/checkXFSfrag.sh
new file mode 100755
index 0000000..f3c71da
--- /dev/null
+++ b/linhes/linhes-system/checkXFSfrag.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+#
+# Bash script by Gene Alexander (http://www.eracc.com/contact)
+# of ERA Computers & Consulting (www.eracc.com, blog.eracc.com, shopping.eracc.com)
+# Written using vim, the BEST plain text file editor in all of Creation.
+#
+# Teach yourself bash scripting: http://tldp.org/LDP/abs/html/index.html
+#
+# Purpose: To check fragmentation on XFS with xfs_db and run xfs_fsr on XFS mount points that
+# are above a specific fragmentation threshold.
+#
+# What is xfs_db? Use 'man xfs_db' to find out.
+# What is xfs_fsr? Use 'man xfs_fsr' to find out.
+#
+# Any busy files, such as open logs on /var/log, will be skipped. To defragment logs one should
+# wrap this script with another script to stop and restart logging. Or, even better, write
+# one's own script just for defragmentation of the logs.
+#
+# Warranty: NONE. Use at your own discretion and be aware that data loss is on your head if
+# you choose to use this script.
+#
+# License: GPL 2.0 http://www.gnu.org/licenses/gpl-2.0.html
+#
+# Suggested Usage: crontab file for root
+# 0 0 * * * /root/bin/chkxfsfrag # Run at midnight
+#
+# Original Release: 2011 December 15 (Merry Christmas!)
+# DO NOT ALTER HEADER FROM THIS LINE UP.
+#
+e='/usr/bin/echo -e' # Use the echo command, not built-in.
+xfsfsr=/usr/bin/xfs_fsr # Set variable with the path to xfs_fsr.
+xfsdb=/usr/bin/xfs_db # Set variable with the path to xfs_db.
+ionice=/usr/bin/ionice # Set variable with the path to ionice.
+idle='/usr/bin/idle.py -s' # Set variable with path to idle.py.
+pctmax=12 # Set maxiumum frag percent needed for defrag.
+ # This is zero here for testing purposes only
+ # a higher number should be used in production.
+array=`df -T|grep xfs|cut -f 1 --delim=" "` # Array of all XFS file systems.
+for i in ${array[@]};
+do
+ #check for idle flag
+ if [[ $1 == "--idle" ]]
+ then
+ while ! $idle
+ do
+ echo "System is busy. Waiting 10 minutes before trying again."
+ sleep 600
+ done
+ fi
+ #check that the device is SATA and skip defrag on SSDs
+ device=`echo ${i} | cut -f 3 --delim="/" | sed 's/[0-9]//g'`
+ isSATA=`cat /sys/block/${device}/queue/rotational`
+ if [[ $isSATA -eq 1 ]]
+ then
+ percentage=`$xfsdb -c frag -r ${i}|grep factor|cut -f 7 --delim=" "`
+ percent2=`$e $percentage|cut -f 1 --delim=.`
+ if [[ "$percent2" -gt "$pctmax" ]]
+ then
+ $e "${i} is $percentage fragmented. Running defragment on ${i}."
+ # Only uncomment one of the following two lines.
+ #$xfsfsr -v ${i} # Uncomment for verbose defrag.
+ $ionice -c3 $xfsfsr ${i} # Uncomment for quiet defrag.
+ else
+ $e "${i} is $percent2% fragmented and is below the fragmentation threshold of $pctmax%. Skipping."
+ fi
+ else
+ echo "${i} is an SSD. Skipping."
+ fi
+done
+exit 0
diff --git a/linhes/linhes-system/create_media_dirs.sh b/linhes/linhes-system/create_media_dirs.sh
new file mode 100755
index 0000000..0aa44f5
--- /dev/null
+++ b/linhes/linhes-system/create_media_dirs.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+#script to create the media directories
+#used for building ISO (go.sh) and LiveCD (pre_install.sh)
+#call it like so
+#create_media_dirs.sh /top_level_path
+
+topdir=$1
+
+if [ x$topdir = "x" ]
+then
+ echo "Top level dir is empty"
+ exit 1
+fi
+
+if [ ! -d "$topdir" ]
+then
+ echo "$topdir is not a dir or does not exist"
+ exit 2
+fi
+
+
+
+while read dirname
+do
+ mkdir -p "${topdir}/${dirname}"
+ touch "${topdir}/${dirname}/.media"
+ chown mythtv:users "${topdir}/${dirname}"
+ chmod 775 "${topdir}/${dirname}"
+ chmod 775 "${topdir}/${dirname}/.media"
+done <<EOF
+media/tv
+media/tv/live
+media/gallery
+media/photos
+media/music
+media/games/nes/roms
+media/games/nes/screens
+media/games/nes
+media/games/pc/screens
+media/games/pc
+media/games/snes/roms
+media/games/snes/screens
+media/games/snes
+media/games/xmame/cabs
+media/games/xmame/flyers
+media/games/xmame/hiscores
+media/games/xmame/history
+media/games/xmame/roms
+media/games/xmame/screens
+media/games/xmame
+media/games
+media/video
+media/tmp
+media/archive
+media/recordings
+media/streaming
+media/artwork/trailers
+media/artwork/coverart
+media/artwork/fanart
+media/artwork/musicart
+media/artwork/screenshots
+media/artwork/banners
+media/games/screenshots
+media/games/fanart
+media/games/boxart
+backup
+backup/system_backups
+backup/user_backups
+backup/mythtv_backups
+EOF
+
+chown -R mythtv:users ${topdir}
+chmod -R 775 ${topdir}
+chmod 1777 "${topdir}/media/tmp"
diff --git a/linhes/linhes-system/diskspace.sh b/linhes/linhes-system/diskspace.sh
new file mode 100755
index 0000000..1ab6dd7
--- /dev/null
+++ b/linhes/linhes-system/diskspace.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+### Monitor free disk space
+# Display alert if the free percentage of space is >= $ALERT
+
+#
+# Static Config Variables
+#
+# free space percentage to trigger an alert
+ALERT=90
+
+#
+# Static Binary Paths
+#
+DF='/usr/bin/df'
+GREP='/usr/bin/grep'
+AWK='/usr/bin/awk'
+CUT='/usr/bin/cut'
+HOSTNAME='/usr/bin/hostnamectl hostname'
+DATE='/usr/bin/date'
+MSG_CLIENT='/usr/bin/lh_notify-send'
+
+#
+# Static System Variables
+#
+THIS_HOST=`${HOSTNAME}`
+
+#
+# Check CLI Options
+#
+VERBOSE=false
+OSD=false
+for ARG in "$@" ; do
+ case $ARG in
+ "-v")
+ VERBOSE=true
+ ;;
+ "-osd")
+ OSD=true
+ ;;
+ esac
+done
+
+[ $VERBOSE = true ] && echo "Checking free disk space on ${THIS_HOST}"
+[ $VERBOSE = true ] && echo "Threshold for warning is ${ALERT}%"
+[ $VERBOSE = true ] && echo "------------------------------------------------------------------"
+
+# Dynamic Variables
+#DATE_STR=`${DATE} "+%d-%B-%y @ %H%Mhrs"`
+
+# Call df to find the used percentages. Grep for only local disks (not remote mounts like nfs or smb)
+# Pipe the output to awk to get the needed columns, then start a while loop to process each line.
+$DF -HPl | $GREP -E "^/dev/" | $AWK '{ print $5 " " $6 " " $1 }' | while read OUTPUT ; do
+ USED_PCENT=$(echo ${OUTPUT} | $AWK '{ print $1}' | $CUT -d'%' -f1 ) # Used space as a percentage
+ PARTITION=$(echo ${OUTPUT} | $AWK '{ print $2 }' ) # Mount Point (eg, /home)
+ DEVICE=$(echo ${OUTPUT} | $AWK '{ print $3 }' ) # Device (eg, /dev/sda1 or LABEL or UUID)
+ if [ $VERBOSE = true ] ; then
+ echo -e "${USED_PCENT}% used:\tDevice ${DEVICE} mounted to ${PARTITION}"
+ fi
+ if [ ${USED_PCENT} -ge $ALERT ]; then
+ echo "WARNING: Partition (${PARTITION}) on (${DEVICE}) is ${USED_PCENT}% full on ${THIS_HOST}"
+ if [ $OSD = true ] && { [ ${PARTITION} = / ] || [ ${PARTITION} = /home ] || [ ${PARTITION} = /data/srv/mysql ]; } then
+ $MSG_CLIENT --app-name="Disk Space WARNING" "Partition (${PARTITION}) on (${DEVICE}) is ${USED_PCENT}% full."
+ fi
+ fi
+done
+
+exit 0
diff --git a/linhes/linhes-system/empty_storage_groups.py b/linhes/linhes-system/empty_storage_groups.py
new file mode 100755
index 0000000..708c118
--- /dev/null
+++ b/linhes/linhes-system/empty_storage_groups.py
@@ -0,0 +1,195 @@
+#!/usr/bin/python
+
+import argparse, glob, operator, os, random, shutil, subprocess, sys, signal
+shouldQuit = False
+
+def getFreeSpaceForDir(dir):
+ stats = os.statvfs(dir)
+ return (stats.f_bavail * stats.f_frsize)
+
+def getFreePercentForDir(dir):
+ stats = os.statvfs(dir)
+ total = (stats.f_blocks)
+ avail = (stats.f_bavail)
+ return (total - avail) / float(total)
+
+def getFileSize(fullPath):
+ return os.path.getsize(fullPath)
+
+def sizeof_fmt(num, suffix='B'):
+ for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
+ if abs(num) < 1024.0:
+ return "%3.1f %s%s" % (num, unit, suffix)
+ num /= 1024.0
+ return "%.1f %s%s" % (num, 'Yi', suffix)
+
+def signal_handler(signal, frame):
+ print("\nWill quit when file has been moved.\nMoving File...")
+ global shouldQuit
+ shouldQuit = True
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c', '--checkonly', action='store_true', help="Check only, don't move any files.")
+ cmdargs = parser.parse_args()
+
+ SGs = []
+ SGDIRS = []
+ SGgrp = "Default"
+ SGselectdata = []
+
+ signal.signal(signal.SIGINT, signal_handler)
+
+ print("\nEmpty a MythTV Storage Group Directory\nPress Ctrl+C to quit")
+
+ # Get Storage Groups from MythDB
+ try:
+ from MythTV import MythDB
+ mythDB = MythDB()
+ records = mythDB.getStorageGroup()
+ recs = mythDB.getStorageGroup()
+ except:
+ print("Couldn't connect to MythTV database.")
+ sys.exit(1)
+
+ # get list of non duplicate SGs
+ for record in records:
+ SGs.append(record.groupname)
+ SGs=list(set(SGs))
+ # Move Default to top of list
+ if "Default" in SGs:
+ SGs.remove("Default")
+ SGs.insert(0,"Default")
+
+ # ask user which SG to use
+ print("\n------------------------------------------------")
+ print("Storage Groups:")
+ for i,sg in enumerate(SGs):
+ print(str(i+1) + ": " + sg)
+
+ try:
+ SGselect=input("\nEnter the number of the storage group to use (default 1): ") or 1
+ SGselect=int(SGselect)
+ if SGselect > len(SGs) or SGselect < 1:
+ SGselect=int("e")
+ except ValueError:
+ print("You must enter a number between 1 and " + str(len(SGs)) + ". Exiting.")
+ sys.exit(0)
+
+ SGgrp=SGs[SGselect-1]
+
+ # Get Storage Group directories
+ for record in recs:
+ if record.groupname == SGgrp:
+ dirname = record.dirname
+ SGDIRS.append(dirname)
+
+ # If there are less than 2 directories defined bail as we can't move anything
+ if len(SGDIRS) < 2:
+ print("There are less than 2 directories defined. Exiting.")
+ sys.exit(0)
+
+ while not shouldQuit:
+ SGDIRSdata = []
+ print("\n------------------------------------------------")
+ print("'" + SGgrp + "' Storage Group Directories - Percent Used:")
+ SGcnt=0
+ # Get percent free and size free
+ for directory in SGDIRS:
+ # Check if SG path exists
+ if not os.path.exists(directory):
+ print(" " + directory + " - Not Mounted")
+ continue
+ # Check if SG has data files to move
+ if len(glob.glob1(directory,"*.ts")) or len(glob.glob1(directory,"*.mpg")) or len(glob.glob1(directory,"*.nuv")) or len(glob.glob1(directory,"*.jpg")):
+ freePcent = getFreePercentForDir(directory)
+ freeSize = getFreeSpaceForDir(directory)
+ SGDIRSdata.append([directory, freePcent, freeSize])
+ SGcnt=SGcnt+1
+ print("%s: %s - %.2f%%" % (SGcnt, directory, freePcent * 100))
+ else:
+ # Check if the selected SG dir has no data files exit
+ if SGselectdata and SGselectdata[0] == directory:
+ print("\n'" + SGgrp + "' Storage Group directories have no files to move. Exiting")
+ sys.exit(0)
+
+ # Exit if no SGs with data found
+ if SGcnt is 0:
+ print("\n'" + SGgrp + "' Storage Group directories have no files to move. Exiting.")
+ sys.exit(0)
+
+ # Ask user to select which SG to empty if not already selected
+ if not SGselectdata:
+ try:
+ SGDIRselect=int(input("\nEnter the number of the storage group directory to empty: "))
+ if SGDIRselect > SGcnt or SGDIRselect < 1:
+ SGDIRselect=int("e")
+ except ValueError:
+ print("You must enter a number between 1 and %s. Exiting." %SGcnt)
+ sys.exit(0)
+
+ SGselectdata=SGDIRSdata[SGDIRselect-1]
+
+ # Sort data on percent free
+ SGDIRSdata = sorted(SGDIRSdata, reverse=True, key=operator.itemgetter(1))
+ leastFull = SGDIRSdata[-1]
+
+ # Make sure leastFull and SGselectdata are not the same dir
+ if leastFull[0] == SGselectdata[0]:
+ leastFull = SGDIRSdata[-2]
+
+ # Get random file from user selected dir
+ fileToMove = random.choice([f for f in os.listdir(SGselectdata[0]) if f.endswith(".ts") or f.endswith(".mpg") or f.endswith(".nuv") or f.endswith(".jpg")])
+ filePathToMove = SGselectdata[0] + "/" + fileToMove
+
+ # Check that the file isn't too big for least used dir
+ fileSize = getFileSize(filePathToMove)
+ if (fileSize > getFreeSpaceForDir(leastFull[0])):
+ # Too big to move
+ print(filePathToMove + " is too big to move to " + leastFull[0])
+ sys.exit()
+
+ print("------------------------------------------------")
+ print("Move File:")
+ print(" " + filePathToMove)
+ print(" Size: " + sizeof_fmt(os.path.getsize(filePathToMove)))
+ print("To:")
+ print(" " + leastFull[0])
+ print(" Available: " + sizeof_fmt(getFreeSpaceForDir(leastFull[0])))
+
+ # Move file
+ if cmdargs.checkonly:
+ print("------------------------------------------------")
+ print("Check Only option was used. No files were moved.")
+ shouldQuit = True
+ else:
+ print("------------------------------------------------")
+ print("Checking System Status...")
+ if subprocess.call(["/usr/bin/python", "/usr/bin/idle.py", "-s"]):
+ print(" System is busy. The file will not be moved.")
+ sys.exit()
+ print("Moving File...")
+ try:
+ shutil.move(filePathToMove, leastFull[0])
+ # eg. src and dest are the same file
+ except shutil.Error as e:
+ a=input("\n%s. Overwrite destination (y/n)? " % e)
+ if a == "y" or a == "Y":
+ os.remove(leastFull[0] + "/" + fileToMove)
+ shutil.move(filePathToMove, leastFull[0])
+ else:
+ b=input("\nRemove %s (y/n)? " % filePathToMove)
+ if b == "y" or b == "Y":
+ os.remove(filePathToMove)
+ # eg. source or destination doesn't exist
+ except IOError as e:
+ print(('Error: %s' % e.strerror))
+
+ # Remove png files for Default & LiveTV SGs
+ if SGgrp == "Default" or SGgrp == "LiveTV":
+ print("------------------------------------------------")
+ print("Removing png Files:")
+ pngFiles = glob.glob(filePathToMove + "*.png")
+ for p in pngFiles:
+ os.remove(p)
+ print(" " + p)
diff --git a/linhes/linhes-system/enableIRWake.sh b/linhes/linhes-system/enableIRWake.sh
new file mode 100755
index 0000000..8a9847a
--- /dev/null
+++ b/linhes/linhes-system/enableIRWake.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+for vendProdID in `lsusb | sed -e 's/.*ID \([a-f0-9]\+:[a-f0-9]\+\).*/\1/g'`
+do
+ foundRemote=`grep -i "$vendProdID" /usr/share/linhes/templates/remotes/receiver_usb.id`
+ if [[ $? = 0 ]]
+ then
+ #echo Found: $foundRemote
+ vendID=`echo $vendProdID | cut -d":" -f1`
+ prodID=`echo $vendProdID | cut -d":" -f2`
+ for usbDevice in `grep . /sys/bus/usb/devices/*/power/wakeup | cut -d"/" -f6`
+ do
+ foundVendID=`cat /sys/bus/usb/devices/$usbDevice/idVendor`
+ foundProdID=`cat /sys/bus/usb/devices/$usbDevice/idProduct`
+ if [[ $foundVendID == $vendID && $foundProdID == $prodID ]]
+ then
+ echo "Enable wake for $foundRemote on $usbDevice"
+ sudo sh -c "echo 'enabled' > /sys/bus/usb/devices/$usbDevice/power/wakeup"
+ fi
+ done
+ fi
+done
diff --git a/linhes/linhes-system/find_orphans.py b/linhes/linhes-system/find_orphans.py
new file mode 100755
index 0000000..786f01b
--- /dev/null
+++ b/linhes/linhes-system/find_orphans.py
@@ -0,0 +1,250 @@
+#!/usr/bin/python3
+# based on https://www.mythtv.org/wiki/Find_orphans.py
+
+from MythTV import MythDB, MythBE, Recorded, MythError
+from socket import timeout
+
+import os
+import sys
+
+def human_size(s):
+ s = float(s)
+ o = 0
+ while s > 1000:
+ s /= 1000
+ o += 1
+ return str(round(s,1))+('B ','KB','MB','GB','TB')[o]
+
+class File( str ):
+ def __new__(self, host, group, path, name, size):
+ return str.__new__(self, name)
+ def __init__(self, host, group, path, name, size):
+ self.host = host
+ self.group = group
+ self.path = path
+ self.size = int(size)
+ def pprint(self):
+ name = '%s: %s' % (self.host, os.path.join(self.path, self))
+ print(' {0:<90}{1:>8}'.format(name, human_size(self.size)))
+ def delete(self):
+ be = MythBE(self.host, db=DB)
+ be.deleteFile(self, self.group)
+
+class MyRecorded( Recorded ):
+ _table = 'recorded'
+ def pprint(self):
+ name = '{0.hostname}: {0.title}'.format(self)
+ if self.subtitle:
+ name += ' - '+self.subtitle
+ print(' {0:<70}{1:>28}'.format(name,self.basename))
+
+def printrecs(title, recs):
+ print(title)
+ for rec in sorted(recs, key=lambda x: x.title):
+ rec.pprint()
+ print('{0:>87}{1:>12}'.format('Count:',len(recs)))
+
+def printfiles(title, files):
+ print(title)
+ for f in sorted(files, key=lambda x: x.path):
+ f.pprint()
+ size = sum([f.size for f in files])
+ print('{0:>87}{1:>12}'.format('Total:',human_size(size)))
+
+def populate(host=None):
+ unfiltered = []
+ kwargs = {'livetv':True}
+ if host:
+ with DB as c:
+ c.execute("""SELECT count(1) FROM settings
+ WHERE hostname=%s AND value=%s""",
+ (host, 'BackendServerAddr'))
+ if c.fetchone()[0] == 0:
+ raise Exception('Invalid hostname specified on command line.')
+ hosts = [host]
+ kwargs['hostname'] = host
+ else:
+ with DB as c:
+ c.execute("""SELECT hostname FROM settings
+ WHERE value='BackendServerAddr'""")
+ hosts = [r[0] for r in c.fetchall()]
+ for host in hosts:
+ for sg in DB.getStorageGroup():
+ if sg.groupname in ('Videos','Banners','Coverart','Fanart',\
+ 'Music','MusicArt', 'Photographs',\
+ 'Screenshots','Trailers'):
+ continue
+ try:
+ dirs,files,sizes = BE.getSGList(host, sg.groupname, sg.dirname)
+ for f,s in zip(files,sizes):
+ newfile = File(host, sg.groupname, sg.dirname, f, s)
+ if newfile not in unfiltered:
+ unfiltered.append(newfile)
+ except:
+ pass
+
+ recs = list(DB.searchRecorded(**kwargs))
+
+ zerorecs = []
+ orphvids = []
+ for rec in list(recs):
+ if rec.basename in unfiltered:
+ recs.remove(rec)
+ i = unfiltered.index(rec.basename)
+ f = unfiltered.pop(i)
+ if f.size < 1024:
+ zerorecs.append(rec)
+ name = rec.basename.rsplit('.',1)[0]
+ for f in list(unfiltered):
+ if name in f:
+ unfiltered.remove(f)
+ for f in list(unfiltered):
+ if not (f.endswith('.mpg') or f.endswith('.nuv') or f.endswith('.ts')):
+ continue
+ orphvids.append(f)
+ unfiltered.remove(f)
+
+ orphimgs = []
+ for f in list(unfiltered):
+ if not f.endswith('.png'):
+ continue
+ orphimgs.append(f)
+ unfiltered.remove(f)
+
+ dbbackup = []
+ for f in list(unfiltered):
+ if 'sql' not in f:
+ continue
+ dbbackup.append(f)
+ unfiltered.remove(f)
+
+ return (recs, zerorecs, orphvids, orphimgs, dbbackup, unfiltered)
+
+def delete_recs(recs):
+ printrecs('The following recordings will be deleted', recs)
+ print('Are you sure you want to continue? (yes/no)')
+ try:
+ res = input('> ')
+ while True:
+ if res == 'yes':
+ for rec in recs:
+ rec.delete(True, True)
+ break
+ elif res == 'no':
+ break
+ else:
+ res = input("'yes' or 'no' > ")
+ except MythError:
+ name = '{0.hostname}: {0.title}'.format(rec)
+ if rec.subtitle:
+ name += ' - '+rec.subtitle
+ print("Warning: Failed to delete '" + name + "'")
+ except KeyboardInterrupt:
+ pass
+ except EOFError:
+ sys.exit(0)
+
+def delete_files(files):
+ printfiles('The following files will be deleted', files)
+ print('Are you sure you want to continue? (yes/no)')
+ try:
+ res = input('> ')
+ while True:
+ if res == 'yes':
+ for f in files:
+ f.delete()
+ break
+ elif res == 'no':
+ break
+ else:
+ res = input("'yes' or 'no' > ")
+ except KeyboardInterrupt:
+ pass
+ except EOFError:
+ sys.exit(0)
+
+def main(host=None):
+ while True:
+ recs, zerorecs, orphvids, orphimgs, dbbackup, unfiltered = populate(host)
+
+ if len(recs):
+ printrecs("Recordings with missing files", recs)
+ if len(zerorecs):
+ printrecs("Zero byte recordings", zerorecs)
+ if len(orphvids):
+ printfiles("Orphaned video files", orphvids)
+ if len(orphimgs):
+ printfiles("Orphaned snapshots", orphimgs)
+ if len(dbbackup):
+ printfiles("Database backups", dbbackup)
+ if len(unfiltered):
+ printfiles("Other files", unfiltered)
+ if not printOnly:
+ opts = []
+ if len(recs):
+ opts.append(['Delete orphaned recording entries', delete_recs, recs])
+ if len(zerorecs):
+ opts.append(['Delete zero byte recordings', delete_recs, zerorecs])
+ if len(orphvids):
+ opts.append(['Delete orphaned video files', delete_files, orphvids])
+ if len(orphimgs):
+ opts.append(['Delete orphaned snapshots', delete_files, orphimgs])
+ if len(unfiltered):
+ opts.append(['Delete other files', delete_files, unfiltered])
+ opts.append(['Refresh list', None, None])
+ print('Please select from the following:')
+ for i, opt in enumerate(opts):
+ if opt[0] == "Refresh list":
+ print(' R. {1}'.format(i+1, opt[0]))
+ refreshNum=i+1
+ else:
+ print(' {0}. {1}'.format(i+1, opt[0]))
+
+ print(' Q. Quit')
+ try:
+ inner = True
+ res = input('> ')
+ while inner:
+ try:
+ res = int(res)
+ except:
+ if res == "Q" or res == "q":
+ sys.exit(0)
+ elif res == "R" or res == "r":
+ res = refreshNum
+ else:
+ res = input('Invalid selection > ')
+ continue
+ if (res <= 0) or (res > len(opts)):
+ res = input('Invalid selection > ')
+ continue
+ break
+ opt = opts[res-1]
+ if opt[1] is None:
+ continue
+ else:
+ opt[1](opt[2])
+
+ except KeyboardInterrupt:
+ break
+ except EOFError:
+ sys.exit(0)
+ else:
+ sys.exit(0)
+DB = MythDB()
+BE = MythBE(db=DB)
+DB.searchRecorded.handler = MyRecorded
+DB.searchRecorded.dbclass = MyRecorded
+
+if __name__ == '__main__':
+ global printOnly
+ if "--printonly" in sys.argv :
+ printOnly=True
+ else:
+ printOnly=False
+
+
+ if len(sys.argv) == 2 and sys.argv[1] != "--printonly":
+ main(sys.argv[1])
+ else:
+ main()
diff --git a/linhes/linhes-system/flatpak_update.cron b/linhes/linhes-system/flatpak_update.cron
new file mode 100644
index 0000000..8d0d83a
--- /dev/null
+++ b/linhes/linhes-system/flatpak_update.cron
@@ -0,0 +1,2 @@
+#!/bin/bash
+flatpak update --noninteractive --assumeyes
diff --git a/linhes/linhes-system/fstrim.hook b/linhes/linhes-system/fstrim.hook
new file mode 100644
index 0000000..3f80657
--- /dev/null
+++ b/linhes/linhes-system/fstrim.hook
@@ -0,0 +1,9 @@
+[Trigger]
+Operation = Install
+Type = Package
+Target = linhes-system
+
+[Action]
+Description = Enable fstrim.timer...
+When = PostTransaction
+Exec = /usr/bin/systemctl enable fstrim.timer
diff --git a/linhes/linhes-system/gen_lib_xml.py b/linhes/linhes-system/gen_lib_xml.py
new file mode 100755
index 0000000..8169f4e
--- /dev/null
+++ b/linhes/linhes-system/gen_lib_xml.py
@@ -0,0 +1,115 @@
+#! /usr/bin/python
+#Helper program that generates library.xml thats custom to linhes.
+#Contents are read from /etc/gen_lib_xml.d
+#This script should be run everytime an entry is added or removed
+
+
+import os, sys, subprocess
+import glob
+
+class Gen_lib_xml:
+ def __init__(self,snippit_dir,orig_theme_file):
+ self.snippit_dir = snippit_dir
+ self.orig_theme_file = orig_theme_file
+ self.xml_snippets=[]
+ self.orig_theme_xml=[]
+ self.new_xml=[]
+
+ def get_new_xml(self):
+ return self.new_xml
+ def get_orig_xml(self):
+ return self.orig_theme_xml
+ def get_snippits(self):
+ return self.xml_snippets
+
+ def read_snippets(self):
+ xml_snippets=""
+ lines=[]
+ try:
+ os.chdir(self.snippit_dir)
+ except:
+ print(" gen_lib_xml: Couldn't change dir to %s" %self.snippit_dir)
+ print(" Exiting")
+ sys.exit(0)
+ file_list=glob.glob("*.conf")
+ for conf_file in file_list:
+ try:
+ print(" gen_lib_xml: reading in %s" %conf_file)
+ f=open(conf_file,'r')
+ line=f.readlines()
+ f.close()
+ except:
+ print(" gen_lib_xml: Couldn't open %s for reading" %conf_file)
+ print(" Exiting")
+ sys.exit(0)
+ lines.extend(line)
+
+ if len(file_list) == 0:
+ print(" gen_lib_xml: no conf files found")
+ lines = []
+ self.xml_snippets = lines
+
+ def read_orig_xml(self):
+ try:
+ print(" gen_lib_xml: reading in %s" %self.orig_theme_file)
+ f=open(self.orig_theme_file,'r')
+ lines=f.readlines()
+ f.close()
+
+ except:
+ print(" gen_lib_xml: Couldn't open %s for reading" %self.orig_theme_file)
+ print(" Exiting")
+ sys.exit(2)
+ #print lines
+ for i in lines:
+ if i.strip() == "</mythmenu>":
+ lines.remove(i)
+ print(" gen_lib_xml: Removing /mythmenu tag ")
+ break
+ self.orig_theme_xml=lines
+
+ def make_new_xml(self):
+ self.new_xml = self.orig_theme_xml + self.xml_snippets
+ self.new_xml.append("</mythmenu>\n")
+ pass
+
+
+
+
+
+
+
+
+def write_xml(xml,filename):
+ try:
+ f=open(filename, 'w')
+ except:
+ print(" gen_lib_xml: Couldn't open %s" %(filename))
+ print(" Exiting")
+ sys.exit(2)
+ print(" gen_lib_xml: Writing %s" %(filename))
+ for i in xml:
+ f.write(i)
+ f.close()
+
+def main():
+ MYTHHOME=subprocess.check_output("lh_home_check.sh").decode('utf-8').strip()
+ filename="%s/.mythtv/library.xml" %MYTHHOME
+ orig_theme_file="/usr/share/mythtv/themes/defaultmenu/library.xml"
+ lib_xml_dir="/etc/gen_lib_xml.d/"
+
+ lib_xml = Gen_lib_xml(lib_xml_dir,orig_theme_file)
+ lib_xml.read_snippets()
+ lib_xml.read_orig_xml()
+ lib_xml.make_new_xml()
+ lib_xml.make_new_xml()
+ new_xml = lib_xml.get_new_xml()
+
+ #a = lib_xml.get_new_xml()
+ #b = lib_xml.get_orig_xml()
+ #c = lib_xml.get_snippits()
+
+ write_xml(new_xml,filename)
+
+if __name__ == "__main__":
+ main()
diff --git a/linhes/linhes-system/idle.py b/linhes/linhes-system/idle.py
new file mode 100755
index 0000000..778677d
--- /dev/null
+++ b/linhes/linhes-system/idle.py
@@ -0,0 +1,379 @@
+#!/usr/bin/python
+
+import argparse, os, re, subprocess, sys, time
+from datetime import datetime, date, timedelta
+
+def msg(cmdargs,msg):
+ if cmdargs.silent is False:
+ print("%s" %msg)
+
+def mythshutdownlock_check(cmdargs,cursor):
+ if (cmdargs.lock):
+ msg(cmdargs," Checking mythshutdown for lock...")
+ try:
+ cursor.execute("select data from settings where value = 'MythShutdownLock'")
+ results=cursor.fetchone()
+ except:
+ return True
+ lock=results[0]
+ if int(lock) == 0 :
+ msg(cmdargs," mythshutdown is NOT locked.")
+ return True
+ else:
+ msg(cmdargs," mythshutdown is locked.")
+ return False
+ else:
+ return True
+
+def dailywake_check(cmdargs,cursor):
+ if (cmdargs.daily):
+ msg(cmdargs," Checking if in a daily wake period...")
+ dailyWake=False
+ today = date.today()
+ now = datetime.now()
+ try:
+ cursor.execute("select data from settings where value = 'DailyWakeupStartPeriod1'")
+ results=cursor.fetchone()
+ p1Start=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M")
+ cursor.execute("select data from settings where value = 'DailyWakeupEndPeriod1'")
+ results=cursor.fetchone()
+ p1End=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M")
+ cursor.execute("select data from settings where value = 'DailyWakeupStartPeriod2'")
+ results=cursor.fetchone()
+ p2Start=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M")
+ cursor.execute("select data from settings where value = 'DailyWakeupEndPeriod2'")
+ results=cursor.fetchone()
+ p2End=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M")
+ except:
+ print("error")
+ return True
+
+ # Check for time periods that cross midnight
+ if (p1End < p1Start):
+ if (now > p1End):
+ p1End = p1End + timedelta(days=1)
+ else:
+ p1Start = p1Start + timedelta(days=-1)
+ if (p2End < p2Start):
+ if (now > p2End):
+ p2End = p2End + timedelta(days=1)
+ else:
+ p2Start = p2Start + timedelta(days=-1)
+
+ #Check for one of the daily wakeup periods
+ if (p1Start != p1End):
+ if (now >= p1Start and now <= p1End):
+ msg(cmdargs," Currently in daily wake period 1.")
+ return False
+ if (p2Start != p2End):
+ if (now >= p2Start and now <= p2End):
+ msg(cmdargs," Currently in daily wake period 2.")
+ return False
+
+ #Are we about to start a daily wakeup period using the -t TIME var
+ if (p1Start != p1End):
+ delta=p1Start-now
+ if (delta.seconds >= 0 and delta.seconds <= cmdargs.time * 60):
+ msg(cmdargs," Daily wake period 1 will start in less than %s minutes." %cmdargs.time)
+ return False
+ if (p2Start != p2End):
+ delta=p2Start-now
+ if (delta.seconds >= 0 and delta.seconds <= cmdargs.time * 60):
+ msg(cmdargs," Daily wake period 2 will start in less than %s minutes." %cmdargs.time)
+ return False
+
+ msg(cmdargs," Currently NOT in a daily wake period.")
+ return True
+ else:
+ return True
+
+def schemalock_check(cmdargs,cursor):
+ msg(cmdargs," Checking if the schema is locked...")
+ try:
+ cursor.execute("select count(*) from schemalock")
+ results=cursor.fetchone()
+ except:
+ return True
+ schemalock=results[0]
+ if schemalock == 0:
+ msg(cmdargs," The schema is NOT locked.")
+ return True
+ else:
+ msg(cmdargs," The schema is locked.")
+ return False
+
+def in_use(cmdargs,cursor):
+ msg(cmdargs," Checking if programs are in use...")
+ try:
+ cursor.execute("select count(*) from inuseprograms")
+ results=cursor.fetchone()
+ except:
+ return True
+ prginuse=results[0]
+ if prginuse == 0 :
+ msg(cmdargs," Programs are NOT in use.")
+ return True
+ else:
+ msg(cmdargs," %s programs are in use." %prginuse)
+ cursor.execute("select recusage,chanid,lastupdatetime from inuseprograms")
+ results=cursor.fetchall()
+ for i in results:
+ msg(cmdargs," %s - %s - %s" %(i[0],i[1],i[2]))
+ return False
+
+def job_check(cmdargs,cursor):
+ msg(cmdargs," Checking jobqueue for active jobs...")
+ try:
+ cursor.execute("select count(*) from jobqueue where status between 2 and 5")
+ results=cursor.fetchone()
+ except:
+ return True
+ jobs=results[0]
+ if jobs == 0 :
+ msg(cmdargs," No jobs are active.")
+ return True
+ else:
+ msg(cmdargs," Jobs are active.")
+ return False
+
+def upcoming_check(cmdargs,mythBE):
+ msg(cmdargs," Checking for recordings in the next %s minutes..." %cmdargs.time)
+ try:
+ upcoming = mythBE.getUpcomingRecordings()
+ except:
+ msg(cmdargs," Could not get upcoming recordings.")
+ return True
+ time_diff=10000
+ r=0
+ for i in upcoming:
+ r += 1
+ if r > 1:
+ break
+ show=str(i)
+ show=show.strip()
+ showtime=re.split("[-+]\d\d:\d\d",str(i.starttime))[0]
+ now=time.time()
+ rec_time=time.strptime( showtime ,"%Y-%m-%d %H:%M:%S" )
+ r=time.mktime(rec_time)
+ time_diff = ( r - now ) / 60
+
+ if ( time_diff > cmdargs.time) :
+ msg(cmdargs," No recordings starting in %s minutes." %cmdargs.time)
+ return True
+ else:
+ msg(cmdargs," A recording is starting in %s minutes." %int(time_diff))
+ return False
+
+def mfd_check(cmdargs):
+ msg(cmdargs," Checking if mythfilldatabase is running...")
+ with open(os.devnull, "w") as fnull:
+ mythfilldatabase_ret = subprocess.call(["pidof", "mythfilldatabase"], stdout=fnull)
+ if mythfilldatabase_ret == 0 :
+ msg(cmdargs," mythfilldatabase is running.")
+ return False
+ else:
+ msg(cmdargs," mythfilldatabase is NOT running.")
+ return True
+
+def mythtvsetup_check(cmdargs):
+ msg(cmdargs," Checking if mythtv-setup is running...")
+ with open(os.devnull, "w") as fnull:
+ mythsetup_ret = subprocess.call(["pidof", "mythtv-setup"], stdout=fnull)
+ if mythsetup_ret == 0 :
+ msg(cmdargs," mythtv-setup is running.")
+ return False
+ else:
+ msg(cmdargs," mythtv-setup is NOT running.")
+ return True
+
+def userlogins_check(cmdargs):
+ if (cmdargs.logins):
+ u=False
+ msg(cmdargs," Checking for users logged in...")
+ users=subprocess.check_output("who")
+ names=([x.split() for x in users.splitlines()])
+ for i in names:
+ if (i[0] == "mythtv" and i[4] == "(:0)"):
+ msg(cmdargs," Ignoring %s %s" %(i[0],i[4]))
+ else:
+ msg(cmdargs," User logged in: %s %s" %(i[0],i[4]))
+ u=True
+ if u:
+ return False
+ else:
+ return True
+ else:
+ return True
+
+def sambafiles_check(cmdargs):
+ if (cmdargs.sambafiles):
+ msg(cmdargs," Checking if Samba files are in use...")
+ try:
+ smbstatus=subprocess.check_output(["smbstatus", "-L"])
+ except:
+ smbstatus="No locked files"
+ if "No locked files" in smbstatus:
+ msg(cmdargs," Samba files are NOT in use.")
+ return True
+ else:
+ msg(cmdargs," Samba files are in use.")
+ return False
+ else:
+ return True
+
+def mythfe_check(cmdargs,cursor,mythDB):
+ #checks to see if a frontend is considered idle
+ # True means FE is idle
+
+ if ( cmdargs.runningfe ):
+ msg(cmdargs," Checking for running and playing mythfrontends...")
+ else:
+ msg(cmdargs," Checking for playing mythfrontends...")
+ try:
+ cursor.execute("select distinct hostname from settings where hostname is not null;")
+ frontends=cursor.fetchall()
+ except:
+ return True
+
+ for i in frontends:
+ try:
+ msg(cmdargs," Checking %s's mythfrontend status..." %i)
+ frontend = mythDB.getFrontend(''.join(i))
+ if ( cmdargs.runningfe ):
+ msg(cmdargs," %s's mythfrontend is RUNNING." %i)
+ return False
+ location = frontend.sendQuery('Location')
+
+ if location == "standbymode":
+ msg(cmdargs," %s's mythfrontend is in Standby Mode." %i)
+ continue
+
+ if ( location.startswith('Playback ') ):
+ msg(cmdargs," %s's mythfrontend is PLAYING." %i)
+ return False
+ else:
+ msg(cmdargs," %s's mythfrontend is NOT playing." %i)
+
+ if '.xml' in location or 'mainmenu' in location:
+ msg(cmdargs," %s's mythfrontend is in MENUS." %i)
+ else:
+ #FE is not in menus, so it must be active in a plugin
+ msg(cmdargs," %s's mythfrontend is NOT in menus." %i)
+ return False
+ except:
+ msg(cmdargs," Could not connect to %s's mythfrontend." %i)
+
+ if ( cmdargs.runningfe ):
+ msg(cmdargs," mythfrontends are not running or playing or are in menus.")
+ else:
+ msg(cmdargs," mythfrontends are not playing or are in menus.")
+
+ return True
+
+def usage():
+ line = '''
+ idle.py checks if the system is idle.
+ Use idle.py -h to see options.
+
+ idle.py checks these parts of the system in this order to
+ determine if it is idle:
+ - (option -g) users are logged in return busy
+ ignores mythtv (:0) for busy
+ - (option -f) Samba files are in use return busy
+ - (option -l) mythshutdown is locked return busy
+ - (option -d) in a daily wake period or
+ about to start a daily wake period return busy
+ checks the next 15 minutes. -t TIME changes time
+ - schema is locked return busy
+ - there are in use programs return busy
+ - there are active jobs in the job queue return busy
+ - mythfilldatabase is running return busy
+ - mythtv-setup is running return busy
+ - there are upcoming recordings return busy
+ checks the next 15 minutes. -t TIME changes time
+ - (option -r) mythfrontends running return busy
+ - mythfrontends playing back a recording or video return busy
+ - mythfrontends not in menus return busy
+
+ idle.py stops checking and returns false (busy) when the first busy is found.
+ '''
+ print(line)
+ sys.exit(0)
+
+def main(args=[False]):
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-d', '--daily', action='store_true', help='Include daily wake & about to start wake in system busy. (default: daily wake & about to start wake is system idle)')
+ parser.add_argument('-g', '--logins', action='store_true', help='Include user logins in system busy. Ignores mythtv (:0) in system busy.')
+ parser.add_argument('-f', '--sambafiles', action='store_true', help='Include Samba files in use in system busy.')
+ parser.add_argument('-l', '--lock', action='store_true', help='Include mythshutdown lock in system busy. (default: mythshutdown lock is system idle)')
+ parser.add_argument('-r', '--runningfe', action='store_true', help='Include running mythfrontends in system busy. (default: running mythfrontends are system idle)')
+ parser.add_argument('-s', '--silent', action='store_true', help='Run without printing output. Recommended for use in cron jobs or scripts.')
+ parser.add_argument('-t', '--time', type=int, default=15, help='Minutes of idle time needed to return idle for upcoming recordings and daily wake.')
+ parser.add_argument('-u', '--usage', action='store_true', help='Print usage instructions.')
+ if args[0] is False:
+ cmdargs = parser.parse_args()
+ else:
+ cmdargs = parser.parse_args(args)
+
+ if cmdargs.usage:
+ usage()
+ idle=True
+ msg(cmdargs,"Checking system idle...")
+
+ if (userlogins_check(cmdargs)):
+ idle = True
+ else:
+ idle = False
+
+ if (idle and sambafiles_check(cmdargs)):
+ idle = True
+ else:
+ idle = False
+
+ try:
+ from MythTV import MythDB
+ mythDB = MythDB()
+ cursor = mythDB.cursor()
+ db_conn=True
+ except:
+ msg(cmdargs,"Couldn't connect to MythTV database.")
+ db_conn=False
+
+ try:
+ from MythTV import MythBE
+ mythBE = MythBE()
+ be_conn=True
+ except:
+ msg(cmdargs,"Couldn't connect to MythTV backend.")
+ be_conn=False
+
+ if ( db_conn and idle ):
+ if (mythshutdownlock_check(cmdargs,cursor) and dailywake_check(cmdargs,cursor) and schemalock_check(cmdargs,cursor) and in_use(cmdargs,cursor) and job_check(cmdargs,cursor)):
+ idle=True
+ else:
+ idle=False
+
+ if ( be_conn and idle ):
+ if (mfd_check(cmdargs) and mythtvsetup_check(cmdargs) and upcoming_check(cmdargs,mythBE)):
+ idle=True
+ else:
+ idle=False
+
+ if ( db_conn and idle ):
+ if (mythfe_check(cmdargs,cursor,mythDB)):
+ idle=True
+ else:
+ idle=False
+
+ if ( idle ):
+ msg(cmdargs,"System is idle.")
+ else:
+ msg(cmdargs,"System is busy.")
+ return idle
+
+if __name__ == "__main__":
+ idle=main()
+ if ( idle ):
+ exit(0)
+ else:
+ exit(1)
diff --git a/linhes/linhes-system/jobqueue_helper.py b/linhes/linhes-system/jobqueue_helper.py
new file mode 100755
index 0000000..6a567cb
--- /dev/null
+++ b/linhes/linhes-system/jobqueue_helper.py
@@ -0,0 +1,63 @@
+#!/usr/bin/python
+
+import argparse, os, re, subprocess, sys, time
+from MythTV import MythDB, Job
+
+mythDB = MythDB()
+cursor = mythDB.cursor()
+
+def set_cmds(cmdargs,job):
+ #print "Setting cmds on job %s to %s" %(cmdargs.jobid,cmdargs.cmd)
+ cursor.execute("update jobqueue set cmds = '%s' where id = '%s'" %(cmdargs.cmd,cmdargs.jobid))
+
+def set_comment(cmdargs,job):
+ #print "Setting comment on job %s to %s" %(cmdargs.jobid,cmdargs.comment)
+ job.setComment("%s" %cmdargs.comment)
+
+def set_status(cmdargs,job):
+ #print "Setting status on job %s to %s" %(cmdargs.jobid,cmdargs.status)
+ job.setStatus("%s" %cmdargs.status)
+
+def run_cursor(cmdargs):
+ cursor.execute("%s" %cmdargs.man_cursor)
+ results=cursor.fetchone()
+ print(results[0])
+
+def usage():
+ line = '''
+ jobqueue_helper.py provides MythTV job queue functions
+ using python bindings for bash scripts.
+ Use jobqueue_helper.py -h to see options.
+ '''
+ print(line)
+ sys.exit(0)
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-cs', '--comment_set', action='store', dest='comment', help='Set the comment of the jobid')
+ parser.add_argument('-cmds', '--cmd_set', type=int, default=77777, action='store', dest='cmd', help='Set the cmd of the jobid')
+ parser.add_argument('-ss', '--status_set', type=int, action='store', dest='status', help='Set the status of the jobid')
+ action = parser.add_mutually_exclusive_group(required=True)
+ action.add_argument('-j', '--jobid', type=int, help='jobid of the job to control')
+ action.add_argument('-m', '--man_cursor', action='store', dest='man_cursor', help='Manual mysql cursor command')
+ action.add_argument('-u', '--usage', action='store_true', help='Print usage instructions.')
+
+ cmdargs = parser.parse_args()
+
+ if cmdargs.usage:
+ usage()
+
+ if cmdargs.jobid:
+ job = Job(cmdargs.jobid)
+
+ if cmdargs.comment:
+ set_comment(cmdargs, job)
+
+ if cmdargs.status:
+ set_status(cmdargs, job)
+
+ if cmdargs.cmd != 77777:
+ set_cmds(cmdargs, job)
+
+ if cmdargs.man_cursor:
+ run_cursor(cmdargs)
diff --git a/linhes/linhes-system/lh_apply_UI_settings.sh b/linhes/linhes-system/lh_apply_UI_settings.sh
new file mode 100755
index 0000000..2cf98a1
--- /dev/null
+++ b/linhes/linhes-system/lh_apply_UI_settings.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+cp /usr/share/linhes/templates/plasma-org.kde.plasma.desktop-appletsrc ~/.config/
+plasma-apply-colorscheme BreezeDark
+kwriteconfig5 --group KDE --key SingleClick false
+kwriteconfig5 --file ~/.config/kscreenlockerrc --group Daemon --key Autolock false
+kwriteconfig5 --file ~/.config/kscreenlockerrc --group Daemon --key LockOnResume false
+kwriteconfig5 --file ~/.config/ksmserverrc --group General --key loginMode emptySession
+kwriteconfig5 --file ~/.config/powermanagementprofilesrc --group AC --group DPMSControl --key idleTime --delete
+kwriteconfig6 --file ~/.powerdevilrc --group AC --group Display --key DimDisplayWhenIdle false
+kwriteconfig6 --file ~/.powerdevilrc --group AC --group Display --key TurnOffDisplayWhenIdle false
+kwriteconfig6 --file ~/.powerdevilrc --group AC --group SuspendAndShutdown --key AutoSuspendAction 0
+
+plasma-apply-wallpaperimage /usr/share/linhes/templates/lights-bud-abstract-4k-cq.jpg
+
+#sddm settings
+if grep -Fxq "User=km" /etc/sddm.conf.d/autologin.conf; then
+ echo "Updating /etc/sddm.conf.d/autologin.conf"
+ echo "[Autologin]" | sudo tee /etc/sddm.conf.d/autologin.conf
+ echo "User=$(whoami)" | sudo tee -a /etc/sddm.conf.d/autologin.conf
+ echo "Session=plasmax11" | sudo tee -a /etc/sddm.conf.d/autologin.conf
+fi
+
+if [ -f "/etc/sddm.conf" ]; then
+ sudo rm /etc/sddm.conf
+fi
+
+if [ ! -f "/etc/sddm.conf.d/theme.conf" ]; then
+ echo "[General]" | sudo tee /etc/sddm.conf.d/theme.conf
+ echo "Numlock=on" | sudo tee -a /etc/sddm.conf.d/theme.conf
+
+ echo "[Theme]" | sudo tee /etc/sddm.conf.d/theme.conf
+ echo "Current=breeze" | sudo tee -a /etc/sddm.conf.d/theme.conf
+ echo "CursorTheme=breeze_cursors" | sudo tee -a /etc/sddm.conf.d/theme.conf
+ echo "Font=Noto Sans,10,-1,0,400,0,0,0,0,0,0,0,0,0,0,1" | sudo tee -a /etc/sddm.conf.d/theme.conf
+fi
+
+#disable file indexing
+balooctl6 disable
diff --git a/linhes/linhes-system/lh_home_check.sh b/linhes/linhes-system/lh_home_check.sh
new file mode 100755
index 0000000..de4e089
--- /dev/null
+++ b/linhes/linhes-system/lh_home_check.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+#Detect the name of the display in use
+display=":$(ls /tmp/.X11-unix/* | sed 's#/tmp/.X11-unix/X##' | head -n 1)"
+
+#Detect the user using such display
+user=$(who | grep '('$display')' | awk '{print $1}' | head -n 1)
+
+echo /home/$user
diff --git a/linhes/linhes-system/lh_lighttpd.conf b/linhes/linhes-system/lh_lighttpd.conf
new file mode 100644
index 0000000..71709a2
--- /dev/null
+++ b/linhes/linhes-system/lh_lighttpd.conf
@@ -0,0 +1,295 @@
+#######################################################################
+# lighttpd configuration file
+#######################################################################
+
+## modules to load
+# at least mod_access and mod_accesslog should be loaded
+# all other module should only be loaded if really neccesary
+# - saves some time
+# - saves memory
+server.modules = (
+ "mod_rewrite",
+# "mod_redirect",
+ "mod_alias",
+ "mod_access",
+# "mod_cml",
+# "mod_trigger_b4_dl",
+ "mod_auth",
+ "mod_status",
+ "mod_setenv",
+ "mod_fastcgi",
+ "mod_proxy",
+# "mod_simple_vhost",
+# "mod_evhost",
+# "mod_userdir",
+ "mod_cgi",
+# "mod_compress",
+ "mod_ssi",
+# "mod_usertrack",
+# "mod_expire",
+# "mod_secdownload",
+# "mod_rrdtool",
+ "mod_accesslog" )
+
+## a static document-root, for virtual-hosting take look at the
+## server.virtual-* options
+server.document-root = "/data/srv/httpd/htdocs"
+
+## where to send error-messages to
+#server.errorlog = "/var/log/lighttpd/error.log"
+#### accesslog module
+#accesslog.filename = "/var/log/lighttpd/access.log"
+
+##send errors to syslog
+server.errorlog-use-syslog = "enable"
+$HTTP["url"] !~ "\.(jpe?g|png|gif|css)" { accesslog.use-syslog = "enable" }
+
+## File uploads
+# Make sure this folder exists and is writable to server.username
+# Add /data/storage/disk0/media/tmp/ for large file downloads in MythWeb
+server.upload-dirs = ( "/var/tmp/", "/data/storage/disk0/media/tmp/" )
+
+# files to check for if .../ is requested
+index-file.names = ( "index.php", "index.html",
+ "index.htm", "default.htm" , "mythweb.php" )
+
+## set the event-handler (read the performance section in the manual)
+# server.event-handler = "freebsd-kqueue" # needed on OS X
+
+# mimetype mapping
+mimetype.assign = (
+".pdf" => "application/pdf",
+".sig" => "application/pgp-signature",
+".spl" => "application/futuresplash",
+".class" => "application/octet-stream",
+".ps" => "application/postscript",
+".torrent" => "application/x-bittorrent",
+".dvi" => "application/x-dvi",
+".gz" => "application/x-gzip",
+".pac" => "application/x-ns-proxy-autoconfig",
+".swf" => "application/x-shockwave-flash",
+".tar.gz" => "application/x-tgz",
+".tgz" => "application/x-tgz",
+".tar" => "application/x-tar",
+".zip" => "application/zip",
+".mp3" => "audio/mpeg",
+".m3u" => "audio/x-mpegurl",
+".wma" => "audio/x-ms-wma",
+".wax" => "audio/x-ms-wax",
+".ogg" => "application/ogg",
+".wav" => "audio/x-wav",
+".gif" => "image/gif",
+".jar" => "application/x-java-archive",
+".jpg" => "image/jpeg",
+".jpeg" => "image/jpeg",
+".png" => "image/png",
+".svg" => "image/svg+xml",
+".xbm" => "image/x-xbitmap",
+".xpm" => "image/x-xpixmap",
+".xwd" => "image/x-xwindowdump",
+".css" => "text/css",
+".html" => "text/html",
+".shtml" => "text/html",
+".htm" => "text/html",
+".js" => "text/javascript",
+".asc" => "text/plain",
+".c" => "text/plain",
+".cpp" => "text/plain",
+".log" => "text/plain",
+".conf" => "text/plain",
+".text" => "text/plain",
+".txt" => "text/plain",
+".dtd" => "text/xml",
+".xml" => "text/xml",
+".mpeg" => "video/mpeg",
+".mpg" => "video/mpeg",
+".mp4" => "video/quicktime",
+".mov" => "video/quicktime",
+".qt" => "video/quicktime",
+".avi" => "video/x-msvideo",
+".asf" => "video/x-ms-asf",
+".asx" => "video/x-ms-asf",
+".wmv" => "video/x-ms-wmv",
+".bz2" => "application/x-bzip",
+".tbz" => "application/x-bzip-compressed-tar",
+".tar.bz2" => "application/x-bzip-compressed-tar",
+# default mime type
+"" => "application/octet-stream",
+)
+
+# Use the "Content-Type" extended attribute to obtain mime type if possible
+#mimetype.use-xattr = "enable"
+
+
+## send a different Server: header
+## be nice and keep it at lighttpd
+# server.tag = "lighttpd"
+
+
+## deny access the file-extensions
+#
+# ~ is for backupfiles from vi, emacs, joe, ...
+# .inc is often used for code includes which should in general not be part
+# of the document-root
+url.access-deny = ( "~", ".inc" )
+
+$HTTP["url"] =~ "\.pdf$" {
+ server.range-requests = "disable"
+}
+ssi.extension = ( ".shtml" )
+
+##
+# which extensions should not be handle via static-file transfer
+#
+# .php, .pl, .fcgi are most often handled by mod_fastcgi or mod_cgi
+static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
+
+######### Options that are good to be but not neccesary to be changed #######
+
+## bind to port (default: 80)
+#server.port = 81
+
+## bind to localhost (default: all interfaces)
+#server.bind = "127.0.0.1"
+
+## error-handler for status 404
+server.error-handler-404 = "/index.html"
+
+## to help the rc.scripts
+server.pid-file = "/var/run/lighttpd/lighttpd.pid"
+
+
+###### virtual hosts
+##
+## If you want name-based virtual hosting add the next three settings and load
+## mod_simple_vhost
+##
+## document-root =
+## virtual-server-root + virtual-server-default-host + virtual-server-docroot
+## or
+## virtual-server-root + http-host + virtual-server-docroot
+##
+#simple-vhost.server-root = "/srv/http/vhosts/"
+#simple-vhost.default-host = "www.example.org"
+#simple-vhost.document-root = "/htdocs/"
+
+
+##
+## Format: <errorfile-prefix><status-code>.html
+## -> ..../status-404.html for 'File not found'
+#server.errorfile-prefix = "/usr/share/lighttpd/errors/status-"
+#server.errorfile-prefix = "/srv/http/errors/status-"
+
+## virtual directory listings
+#dir-listing.activate = "enable"
+## select encoding for directory listings
+#dir-listing.encoding = "utf-8"
+
+## enable debugging
+#debug.log-request-header = "enable"
+#debug.log-response-header = "enable"
+#debug.log-request-handling = "enable"
+#debug.log-file-not-found = "enable"
+
+### only root can use these options
+#
+# chroot() to directory (default: no chroot() )
+#server.chroot = "/"
+
+## change uid to <uid> (default: don't care)
+server.username = "http"
+
+## change uid to <uid> (default: don't care)
+server.groupname = "http"
+
+#### compress module
+#compress.cache-dir = "/var/cache/lighttpd/compress/"
+#compress.filetype = ("text/plain", "text/html")
+
+#### proxy module
+## read proxy.txt for more info
+#proxy.server = ( ".php" =>
+# ( "localhost" =>
+# (
+# "host" => "192.168.0.101",
+# "port" => 80
+# )
+# )
+# )
+
+#### fastcgi module
+## read fastcgi.txt for more info
+## for PHP don't forget to set cgi.fix_pathinfo = 1 in the php.ini
+$HTTP["url"] =~ "^/mythweb/" {
+ server.document-root = "/data/srv/httpd/mythweb"
+ alias.url += ( "/mythweb" => "/data/srv/httpd/mythweb/")
+ fastcgi.server = (
+ "/mythweb/mythweb.php" => ((
+ "bin-path" => "/usr/bin/php-cgi",
+ "socket" => "/var/run/lighttpd/mythtv-php-fcgi.socket",
+ "broken-scriptfilename" => "enable",
+ "bin-environment" => (
+ "db_server" => "localhost",
+ "db_name" => "mythconverg",
+ "db_login" => "mythtv",
+ "db_password" => "mythtv"
+ )
+ ))
+ )
+
+ setenv.add-environment = (
+ "db_server" => "localhost",
+ "db_name" => "mythconverg",
+ "db_login" => "mythtv",
+ "db_password" => "mythtv"
+)
+}
+
+$HTTP["url"] =~ "^/zabbix/" {
+ server.document-root = "/data/srv/httpd/zabbix"
+ alias.url += ( "/zabbix" => "/data/srv/httpd/zabbix/")
+ fastcgi.server = (
+ ".php" => ((
+ "bin-path" => "/usr/bin/php-cgi",
+ "socket" => "/var/run/lighttpd/zabbix-php-fcgi.socket",
+ "broken-scriptfilename" => "enable",
+ ))
+ )
+}
+
+#### CGI module
+cgi.assign = ( ".pl" => "/usr/bin/perl",
+ ".cgi" => "/usr/bin/perl",
+ ".sh" => "/usr/bin/bash",
+ ".py" => "/usr/bin/python")
+
+alias.url += ( "/cgi-bin" => "/data/srv/httpd/cgi-bin/")
+
+$HTTP["url"] =~ "^/cgi-bin" {
+ cgi.assign = ( "" => "" )
+ }
+
+#### url handling modules (rewrite, redirect, access)
+#url.rewrite = ( "^/$" => "/server-status" )
+#url.redirect = ( "^/wishlist/(.+)" => "http://www.123.org/$1" )
+
+#### setenv
+#setenv.add-request-header = ( "TRAV_ENV" => "mysql://user@host/db" )
+#setenv.add-response-header = ( "X-Secret-Message" => "42" )
+
+#### include
+#include "/etc/lighttpd/auth-inc.conf"
+## same as above if you run: "lighttpd -f /etc/lighttpd/lighttpd.conf"
+#include "lighttpd-inc.conf"
+
+#### include_shell
+#include_shell "echo var.a=1"
+## the above is same as:
+#var.a=1
+
+url.rewrite-once = (
+"^/{1,2}mythweb/(css|data|images|js|themes|skins|[a-z_]+\.(php|pl)).*" => "$0",
+"^/{1,2}mythweb/(pl(/.*)?)$" => "/mythweb/mythweb.pl/$1",
+"^/{1,2}mythweb/(.+)$" => "/mythweb/mythweb.php/$1",
+"^/{1,2}mythweb/(.*)$" => "/mythweb/mythweb.php"
+)
diff --git a/linhes/linhes-system/lh_log_care.cron b/linhes/linhes-system/lh_log_care.cron
new file mode 100755
index 0000000..ace0534
--- /dev/null
+++ b/linhes/linhes-system/lh_log_care.cron
@@ -0,0 +1,16 @@
+#!/bin/bash
+MYTH_RUN_STATUS=1
+. /etc/profile
+
+BackupDir="/var/log/20*-*-*"
+KeepBackups=14
+NumBackups=`ls -d $BackupDir | wc -l`
+
+if [[ $NumBackups > $KeepBackups ]]; then
+ echo "Deleting old log files"
+ numdel=$(($NumBackups-$KeepBackups))
+ rm -rf `ls -d $BackupDir | head -$numdel`
+fi
+
+echo "Compressing log files"
+find $BackupDir -type f -mtime +6 \( ! -iname "*.gz" \) -exec gzip -9 {} \;
diff --git a/linhes/linhes-system/lh_mtc.cron b/linhes/linhes-system/lh_mtc.cron
new file mode 100644
index 0000000..b556d60
--- /dev/null
+++ b/linhes/linhes-system/lh_mtc.cron
@@ -0,0 +1,80 @@
+#!/bin/bash
+MYTH_RUN_STATUS=1
+. /etc/systemconfig
+. /etc/profile
+
+date=`date +%Y-%m-%d`
+timestamp=`date +'%Y-%m-%d %H:%M'`
+hostname=`/usr/bin/hostnamectl hostname`
+logFile="/var/log/${date}/${hostname}_lh_mtc.log"
+log="logger -t lh_mtc -p local6.info"
+
+if [ ! -f $logFile ]; then
+ touch $logFile
+ echo "" | $log
+fi
+
+#check logfile for Finished and if not run lh_mtc.py
+if ! grep -q "Finished Maintenance" $logFile
+then
+
+ if ! grep -q "Finished checking size of MythTV home" $logFile
+ then
+ sudo -u mythtv bash -c "MYTHCONFDIR=/usr/share/mythtv unbuffer lh_mtc.py --check_home | $log"
+ if [ $? = 0 ]
+ then
+ echo "" | $log
+ else
+ echo "Time Exceeded" | $log
+ exit
+ fi
+ fi
+
+ if ! grep -q "Finished Optimize" $logFile
+ then
+ if [ $SystemType = FrontendOnly ]
+ then
+ echo "Will not run Optimize on Frontend Only systems." | $log
+ echo "Finished Optimize" | $log
+ else
+ sudo -u mythtv bash -c "MYTHCONFDIR=/usr/share/mythtv unbuffer lh_mtc.py --optimize | $log"
+ if [ $? = 0 ]
+ then
+ echo "" | $log
+ else
+ echo "Time Exceeded" | $log
+ exit
+ fi
+ fi
+ fi
+
+ if ! grep -q "Finished Backup" $logFile && grep -q "Finished Optimize" $logFile
+ then
+ sudo -u mythtv bash -c "MYTHCONFDIR=/usr/share/mythtv unbuffer lh_mtc.py --backup | $log"
+ if [ $? = 0 ]
+ then
+ echo "" | $log
+ else
+ echo "Time Exceeded" | $log
+ exit
+ fi
+ fi
+
+# if ! grep -q "Finished Update" $logFile
+# then
+# sudo -u mythtv bash -c "MYTHCONFDIR=/usr/share/mythtv unbuffer lh_mtc.py --update | $log"
+# if [ $? = 0 ]
+# then
+# echo "" | $log
+# else
+# echo "Time Exceeded" | $log
+# exit
+# fi
+# fi
+
+# if grep -q "Finished checking size of MythTV home" $logFile && grep -q "Finished Optimize" $logFile && grep -q "Finished Backup" $logFile && grep -q "Finished Update" $logFile
+ if grep -q "Finished checking size of MythTV home" $logFile && grep -q "Finished Optimize" $logFile && grep -q "Finished Backup" $logFile
+ then
+ echo "Finished Maintenance" | $log
+ fi
+fi
diff --git a/linhes/linhes-system/lh_mtc.py b/linhes/linhes-system/lh_mtc.py
new file mode 100755
index 0000000..5a8b75e
--- /dev/null
+++ b/linhes/linhes-system/lh_mtc.py
@@ -0,0 +1,153 @@
+#!/usr/bin/python
+import sys, subprocess
+import re
+import socket
+import os
+import datetime,time
+import shlex
+sys.dont_write_bytecode = True
+
+try:
+ from MythTV import MythBE
+ mythtv = MythBE()
+except:
+ mythtv = None
+
+#print mythtv.db.getSetting( 'Theme', socket.gethostname())
+
+def get_timestamp():
+ now = datetime.datetime.now()
+ date = (now.strftime('%Y-%m-%d %H:%M'))
+ return date
+
+def getFreePercentForDir(dir):
+ stats = os.statvfs(dir)
+ total = (stats.f_blocks)
+ avail = (stats.f_bavail)
+ return (total - avail) / float(total)
+
+def check_home():
+ #get the mythtv home dir
+ MYTHHOME=subprocess.check_output("lh_home_check.sh").decode('utf-8').strip()
+
+ freePcent = getFreePercentForDir(MYTHHOME)
+ print(" Home directory percent used: " + str(freePcent * 100) + "%")
+ if float(freePcent) > .9:
+ print(" Home directory is greater than 90% used. Clearing caches...")
+ cmd = "/usr/bin/rm -rf " + MYTHHOME + "/.mythtv/{*cache,Cache-*,tmp/*,MythMusic/AlbumArt/*}"
+ subprocess.call(["sh", "-c", cmd])
+ print(" Restarting mythfrontend...")
+ subprocess.call(["killall", "mythfrontend"])
+ cmd = "/usr/bin/rm -rf " + MYTHHOME + "/.cache/*"
+ subprocess.call(["sh", "-c", cmd])
+ cmd = "/usr/bin/rm -rf " + MYTHHOME + "/.plexht/userdata/Thumbnails/*"
+ subprocess.call(["sh", "-c", cmd])
+ cmd = "/usr/bin/rm -rf " + MYTHHOME + "/.plexht/userdata/ThemeMusicCache/*"
+ subprocess.call(["sh", "-c", cmd])
+ freePcent = getFreePercentForDir(MYTHHOME)
+ print(" Home directory percent used: " + str(freePcent * 100) + "%")
+ else:
+ print(" Home directory is less than 90% used. Not clearing caches.")
+ return 0
+
+def optimize():
+ try:
+ cursor = mythtv.db.cursor()
+ cursor.execute("SHOW tables")
+ result = cursor.fetchall()
+ except:
+ print("\n%s Problem getting tables from the database" %(get_timestamp()))
+ return 1
+ ops=["REPAIR","OPTIMIZE","ANALYZE"]
+ for row in result:
+ ctable=row[0]
+ for op in ops:
+ print(" %s %s" %(op,ctable))
+ cmd= "%s table %s" %(op,ctable)
+ cursor.execute(cmd)
+ return 0
+
+def cleanup_inuseprograms():
+ fourHoursAgo=datetime.datetime.today() - datetime.timedelta(hours=4)
+ cmd="DELETE FROM inuseprograms WHERE lastupdatetime < '%s';" %fourHoursAgo
+ try:
+ cursor = mythtv.db.cursor()
+ cursor.execute(cmd)
+ except:
+ print("\n%s Problem cleaning inuseprograms in database" %(get_timestamp()))
+
+
+def bail_if_another_is_running():
+ cmd = shlex.split("pgrep -u {} -f {}".format(os.getuid(), __file__))
+ pids = subprocess.check_output(cmd).decode('utf-8').strip().split('\n')
+ if len(pids) > 1:
+ pids.remove("{}".format(os.getpid()))
+ print("Exiting! Found {} is already running (pids): {}".format(
+ __file__, " ".join(pids)))
+ raise SystemExit(1)
+
+def run_stuff():
+ print("\n%s" %get_timestamp())
+
+ if (len(sys.argv) == 1) or ("--noidlecheck" in sys.argv) and (len(sys.argv) == 2):
+ runall = True
+ else:
+ runall = False
+
+ if ("--noidlecheck" in sys.argv):
+ print("No system idle check will be done.")
+ idle = 0
+ else:
+ idle = subprocess.call(["/usr/bin/python", "/usr/bin/idle.py"])
+
+ if not idle:
+ if ("--check_home" in sys.argv) or runall:
+ print("\n#######################################")
+ print("\n%s Checking size of MythTV home" %(get_timestamp()))
+ if not check_home():
+ print("\nFinished checking size of MythTV home")
+ else:
+ return True
+
+ if ("--optimize" in sys.argv) or runall:
+ print("\n#######################################")
+ print("\n%s Running Optimize" %(get_timestamp()))
+ if not optimize():
+ print("\nFinished Optimize")
+ else:
+ return True
+
+ if ("--backup" in sys.argv) or runall:
+ print("\n#######################################")
+ print("\n%s Running Backup" %(get_timestamp()))
+ if not os.system('sudo /usr/bin/lh_system_backup_job'):
+ print("\nFinished Backup")
+
+# if ("--update" in sys.argv) or runall:
+# print("\n#######################################")
+# print("\n%s Running System Update" %(get_timestamp()))
+# if not os.system('/usr/bin/lh_system_host_update'):
+# print("\nFinished Update")
+
+ print("\n#######################################")
+ continue_loop=False
+ else:
+ continue_loop=True
+ return continue_loop
+
+#---------------------------------
+bail_if_another_is_running()
+starttime=time.time()
+ctin=True
+while ctin:
+ cleanup_inuseprograms()
+ ctin=run_stuff()
+ if ctin:
+ print("\n%s Waiting 10 minutes before trying again." %(get_timestamp()))
+ time.sleep(600)
+
+ current_time=time.time()
+ if (current_time - starttime) > 3000 :
+ ctin = False
+ print("\n%s Time Exceeded 50 minutes. Quitting.)" %(get_timestamp()))
+ exit(1)
diff --git a/linhes/linhes-system/lh_myth_status.py b/linhes/linhes-system/lh_myth_status.py
new file mode 100755
index 0000000..c682d1c
--- /dev/null
+++ b/linhes/linhes-system/lh_myth_status.py
@@ -0,0 +1,271 @@
+#!/usr/bin/python
+#This program is called on login to display the status of mythtv tuners & recording status
+#Also will display alerts generated by xymon. If the location of xymon changes, this script needs to be updated.
+
+from MythTV import MythBE,MythDB,MythLog
+import datetime,pytz,re,socket,subprocess,sys,time
+from dateutil.parser import parse
+from tzlocal import get_localzone
+
+import os,glob
+from socket import gethostname;
+
+def formatTD(td):
+ days = td.days
+ hours = td.seconds // 3600
+ minutes = (td.seconds % 3600) // 60
+ seconds = td.seconds % 60
+
+ if days == 0:
+ day_string = ""
+ elif days > 1:
+ day_string = "%s days, " %days
+ else:
+ day_string = "%s day, " %days
+
+ if hours > 1:
+ hour_string = "%s hours, " %hours
+ else:
+ hour_string = "%s hour, " %hours
+
+ if minutes > 1:
+ minute_string = "%s minutes, " %minutes
+ else:
+ minute_string = "%s minute, " %minutes
+
+ if seconds > 1:
+ second_string = "%s seconds" %seconds
+ else:
+ second_string = "%s second" %seconds
+
+ return_string = '%s%s%s%s' % (day_string, hour_string, minute_string, second_string)
+ return return_string
+
+def print_alerts():
+ dir_name = "/home/xymon/var/login_alerts"
+ out_alert=""
+ try:
+ os.chdir(dir_name)
+ except:
+ pass
+ #print " myth_status: Couldn't change dir to %s" %dir_name
+
+ file_list=glob.glob("*")
+
+ if len(file_list) == 0:
+ #print " myth_status: no alert files found"
+ pass
+ else:
+ for alert_file in file_list:
+ out_line=''
+ datahost = ''
+ dataservice = ''
+ datacolor = ''
+ datadown = ''
+ try:
+ #print " myth_staus: reading in %s" %alert_file
+ f=open(alert_file,'r')
+ lines=f.readlines()
+ f.close()
+ except:
+ #print " myth_status: Couldn't open %s for reading" %alert_file
+ continue
+
+ for line in lines:
+ try:
+ data,value=line.split(":")
+ except:
+ continue #exception occured try the next line
+
+ if data == 'HOST':
+ datahost = value.strip()
+ elif data == 'SERVICE':
+ dataservice = value.strip()
+ elif data == 'COLOR':
+ datacolor = value.strip()
+ elif data == 'DOWN':
+ datadown = value.strip()
+ sec=int(datadown)
+ td_sec = datetime.timedelta(seconds=sec)
+ td_sec_formated = formatTD(td_sec)
+
+ out_line =" %s on %s %s for %s \n" %(dataservice,
+ datahost,datacolor.upper(),
+ td_sec_formated)
+ out_alert += out_line
+
+ print("System Alerts:")
+ print("--------------")
+ if len(out_alert) > 0:
+ print(out_alert)
+ print(" Go to http://%s and click Health & Maintenance for more information." %gethostname())
+ else:
+ print(" All systems OK")
+
+ return
+
+
+#-------------------------------------------
+
+
+class tuner_recording_status:
+ def __init__ (self,num_upcoming):
+
+ self.now = datetime.datetime.now(pytz.utc)
+ self.currTZ = get_localzone()
+ self.farout=99999999
+ self.next_start_diff=datetime.timedelta(self.farout)
+ self.num_upcoming=num_upcoming
+ self.tuner_status_list=[]
+ self.conflict_list=[]
+ self.upcoming_list=[]
+ self.ur=0
+ self.db_connection_status = self.check_database_connection()
+ if self.db_connection_status == 0:
+ self.tuner_status()
+ self.conflicts()
+ self.upcoming_recordings()
+
+ def get_db_check_status(self):
+ return self.db_connection_status
+
+ def check_database_connection(self):
+ rc=0
+ try:
+ self.be = MythBE()
+ self.db = MythDB()
+ self.cursor = self.db.cursor()
+ except:
+ print("\nCouldn't connect to MythTV service for status")
+ rc=1
+ return rc
+#-----
+ def tuner_status(self):
+ a=self.be.getRecorderList()
+ for i in a:
+ outline=''
+ cmd="select cardtype,hostname,displayname from capturecard where cardid=%s;" %i
+ self.cursor.execute(cmd)
+ results=self.cursor.fetchall()
+ type = results[0][0]
+ hostname = results[0][1]
+ displayname = results[0][2]
+ id = i
+ try:
+ c=self.be.getCurrentRecording(i)
+ if c.title == None:
+ current_recording = "Idle"
+ else:
+ current_recording = "Recording %s" %c.title
+ outline = " Tuner %s - %s (%s) on %s : %s " %(id, displayname, type, hostname, current_recording)
+ self.tuner_status_list.append(outline)
+ except:
+ outline = " Tuner %s - %s (%s) on %s : %s " %(id, displayname, type, hostname, "Tuner Error")
+ self.tuner_status_list.append(outline)
+
+ def get_tuner_status(self):
+ return self.tuner_status
+
+ def print_tuner_status(self):
+ print("Tuner Status:")
+ print("-------------")
+ if len(self.tuner_status_list) > 0 :
+ for line in self.tuner_status_list:
+ print(line)
+ else:
+ print(" No tuners found")
+#--------
+ def upcoming_recordings(self):
+
+ a=self.be.getUpcomingRecordings()
+ r=0
+ for i in a:
+ r += 1
+ if r > self.num_upcoming:
+ break
+ title_chan="%s (%s)" %(i.title, i.channame)
+ # convert timezone to local timezone
+ start_time=parse(str(i.starttime))
+ start_time=start_time.astimezone(self.currTZ)
+ start_time_out=start_time.strftime("%a %b %d %I:%M%p")
+ self.upcoming_list.append([start_time_out,i.hostname, title_chan])
+
+ diff = start_time - self.now
+ if diff < self.next_start_diff :
+ self.next_start_diff = diff
+
+ if self.next_start_diff == datetime.timedelta(self.farout):
+ self.ur="No recordings are scheduled"
+ else:
+ self.ur=formatTD(self.next_start_diff)
+
+ def get_upcoming_recordings(self):
+ return self.upcoming_list
+
+ def print_upcoming_recordings(self):
+ #print self.get_upcoming_recordings()
+ print("")
+ print("Upcoming Recordings (Next %s Scheduled):" %(self.num_upcoming))
+ print("----------------------------------------")
+ if len(self.get_upcoming_recordings()) > 0:
+ for i in self.get_upcoming_recordings():
+ #print " %s - %s - %s" %(start_time_out,i.hostname, title_chan)
+ print(" %s - %s - %s" %(i[0],i[1],i[2]))
+ else:
+ print(" No upcoming recordings")
+ pass
+
+ def get_next_start_time(self):
+ return self.ur
+
+ def print_next_start_time(self):
+ print("")
+ print("The next recording starts in:")
+ print("-----------------------------")
+ print(" %s" %(self.get_next_start_time()))
+ print("")
+
+#-----
+
+ def conflicts(self):
+ a=self.be.getConflictedRecordings()
+ for i in a:
+ out_line=''
+ title_chan="%s (%s)" %(i.title, i.channame)
+ # convert timezone to local timezone
+ start_time=parse(str(i.starttime))
+ start_time=start_time.astimezone(self.currTZ)
+ start_time_out=start_time.strftime("%a %b %d %I:%M%p")
+ out_line=(start_time_out,i.hostname,title_chan)
+ self.conflict_list.append(out_line)
+
+ def get_conflict_list(self):
+ return self.conflict_list
+
+ def print_conflict_list(self):
+ print("")
+ print("Recording Conflicts:")
+ print("--------------------")
+ if len(self.get_conflict_list()) > 0:
+ for i in self.get_conflict_list():
+ print(" %s - %s - %s" %(i[0],i[1],i[2]))
+ else:
+ print(" No conflicts")
+
+#header="#"*60
+
+
+def go():
+ welcomeFile=open("/etc/LinHES-release", "r")
+ print("Welcome to %s on %s\n" %(welcomeFile.readline().rstrip(), socket.gethostname()))
+ tuner = tuner_recording_status(12)
+ if tuner.get_db_check_status() == 0:
+ tuner.print_tuner_status()
+ tuner.print_upcoming_recordings()
+ tuner.print_conflict_list()
+ tuner.print_next_start_time()
+ #print_alerts()
+
+
+if __name__ == "__main__":
+ go()
diff --git a/linhes/linhes-system/lh_notify-send b/linhes/linhes-system/lh_notify-send
new file mode 100755
index 0000000..dd98ede
--- /dev/null
+++ b/linhes/linhes-system/lh_notify-send
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+#Detect the name of the display in use
+display=":$(ls /tmp/.X11-unix/* | sed 's#/tmp/.X11-unix/X##' | head -n 1)"
+
+#Detect the user using such display
+user=$(who | grep '('$display')' | awk '{print $1}' | head -n 1)
+
+#Detect the id of the user
+uid=$(id -u $user)
+
+sudo -u $user DISPLAY=$display DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus notify-send "$@"
diff --git a/linhes/linhes-system/lh_php.ini b/linhes/linhes-system/lh_php.ini
new file mode 100644
index 0000000..bea2eae
--- /dev/null
+++ b/linhes/linhes-system/lh_php.ini
@@ -0,0 +1,64 @@
+[PHP]
+
+; Log errors to specified file. PHP's default behavior is to leave this value
+; empty.
+; http://php.net/error-log
+; Example:
+;error_log = php_errors.log
+; Log errors to syslog (Event Log on Windows).
+;error_log = syslog
+
+; How many GET/POST/COOKIE input variables may be accepted
+max_input_vars = 6000
+
+; Maximum execution time of each script, in seconds
+; https://php.net/max-execution-time
+; Note: This directive is hardcoded to 0 for the CLI SAPI
+max_execution_time = 300
+
+; idea to limit this time on productions servers in order to eliminate unexpectedly
+; long running scripts.
+; Note: This directive is hardcoded to -1 for the CLI SAPI
+; Default Value: -1 (Unlimited)
+; Development Value: 60 (60 seconds)
+; Production Value: 60 (60 seconds)
+; https://php.net/max-input-time
+max_input_time = 300
+
+; Maximum size of POST data that PHP will accept.
+; Its value may be 0 to disable the limit. It is ignored if POST data reading
+; is disabled through enable_post_data_reading.
+; https://php.net/post-max-size
+post_max_size = 16M
+
+; This directive determines whether or not PHP will recognize code between
+; <? and ?> tags as PHP source which should be processed as such. It is
+; generally recommended that <?php and ?> should be used and that this feature
+; should be disabled, as enabling it may result in issues when generating XML
+; documents, however this remains supported for backward compatibility reasons.
+; Note that this directive does not control the <?= shorthand tag, which can be
+; used regardless of this directive.
+; Default Value: On
+; Development Value: Off
+; Production Value: Off
+; http://php.net/short-open-tag
+short_open_tag = On
+
+; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP's
+; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
+; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting
+; this to 1 will cause PHP CGI to fix its paths to conform to the spec. A setting
+; of zero causes PHP to behave as before. Default is 1. You should fix your scripts
+; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
+; http://php.net/cgi.fix-pathinfo
+cgi.fix_pathinfo=1
+
+extension=bcmath.so
+extension=ftp.so
+extension=gd.so
+extension=gettext.so
+extension=mysqli.so
+;extension=openssl.so
+extension=pdo_mysql.so
+extension=sockets.so
+;extension=zip.so
diff --git a/linhes/linhes-system/lh_restart_needed.hook b/linhes/linhes-system/lh_restart_needed.hook
new file mode 100644
index 0000000..e22ecdc
--- /dev/null
+++ b/linhes/linhes-system/lh_restart_needed.hook
@@ -0,0 +1,9 @@
+[Trigger]
+Operation = Upgrade
+Type = Package
+Target = *
+
+[Action]
+Description = Checking if reboot is needed...
+When = PostTransaction
+Exec = /usr/bin/lh_restart_needed.sh
diff --git a/linhes/linhes-system/lh_restart_needed.sh b/linhes/linhes-system/lh_restart_needed.sh
new file mode 100755
index 0000000..2db8a2d
--- /dev/null
+++ b/linhes/linhes-system/lh_restart_needed.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+get_boot_kernel() {
+ local get_version=0
+ for field in $(file /boot/vmlinuz*); do
+ if [[ $get_version -eq 1 ]]; then
+ echo $field
+ return
+ elif [[ $field == version ]]; then
+ # the next field contains the version
+ get_version=1
+ fi
+ done
+}
+
+rc=1
+
+libs=$(lsof -n +c 0 2> /dev/null | grep 'DEL.*lib' | awk '1 { print $1 ": " $NF }' | sort -u)
+if [[ -n $libs ]]; then
+ cat <<< $libs
+ echo "# LIBS: reboot required"
+ rc=0
+fi
+
+active_kernel=$(uname -r)
+current_kernel=$(get_boot_kernel)
+#echo $active_kernel
+#echo $current_kernel
+
+if [[ $active_kernel != $current_kernel ]]; then
+ echo "$active_kernel < $current_kernel"
+ echo "# KERNEL: reboot required"
+ rc=0
+fi
+
+if [[ $rc == 1 ]]; then
+ echo "No reboot needed."
+fi
+exit $rc
diff --git a/linhes/linhes-system/lh_setup_fileshare.py b/linhes/linhes-system/lh_setup_fileshare.py
new file mode 100755
index 0000000..81eb5fa
--- /dev/null
+++ b/linhes/linhes-system/lh_setup_fileshare.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python
+
+import glob, sys, subprocess, os, re, socket
+
+def setup_samba():
+ mythhome = subprocess.check_output("lh_home_check.sh").decode('utf-8').strip()
+ excludes = ['mysql','srv']
+ print(" Activating samba file sharing")
+ usersamba=mythhome+"/templates/smb.conf"
+ subprocess.call(["pacman", "-S", "--noconfirm", "samba", "avahi"])
+ if not os.path.exists("/etc/samba"):
+ print(" Creating directory /etc/samba")
+ try:
+ os.makedirs("/etc/samba")
+ except:
+ pass
+
+ if os.path.exists(usersamba):
+ print(" Using user provided config file " + usersamba)
+ subprocess.call(["install", "-Dm755", usersamba, "/etc/samba/smb.conf"])
+ else:
+ #Samba_media = systemconfig.get("Samba_media")
+ #Samba_home = systemconfig.get("Samba_home")
+ smreadonly = "yes"
+ shreadonly = "yes"
+ domain = "WORKGROUP"
+ servername = socket.gethostname()
+
+ try:
+ f = open("/usr/share/linhes/templates/smb.conf.template",'r')
+ t_smbconf = f.readlines()
+ f.close()
+ except:
+ print(" Couldn't open samba template file")
+ return
+
+ try:
+ f = open("/etc/samba/smb.conf",'w')
+ except:
+ print(" Couldn't open samba file")
+ return
+
+ for line in t_smbconf:
+ outline = line
+ if re.match("^.*workgroup", line):
+ print(" Setting workgroup to " + domain)
+ outline=" workgroup = %s\n" %domain
+ print(" " + outline)
+ if re.match("^.* server string",line):
+ print(" Setting server name to " + servername)
+ outline=" server string = %s\n" %servername
+ print(" " + outline)
+ f.write(outline)
+
+ outline="include = %s/templates/user.shares \n" %mythhome
+ f.write(outline)
+ outline="include = /etc/samba/smb.conf.media\n"
+ f.write(outline)
+ outline="include = /etc/samba/smb.conf.home\n"
+ f.write(outline)
+ f.close()
+
+ print(" Writing smb.conf.media")
+ try:
+ f = open("/etc/samba/smb.conf.media","w")
+ except:
+ print("* Couldn't open smb.conf.media")
+ return
+
+ shares = scan_for_shares()
+ medialines='''
+[%s]
+path = %s
+public = yes
+only guest = yes
+writeable = %s
+printable = no
+force user = mythtv
+force group = mythtv
+create mask = 0755\n'''
+ new_share=[]
+ excludes
+ for share in shares:
+ share_name = share.split("/")[-1]
+ share_path = share
+ f.write(medialines %(share_name,share_path,smreadonly) )
+ print(medialines %(share_name,share_path,smreadonly) )
+ excludeline = 'veto files = '
+ for exclude in excludes:
+ excludeline+= ''' /%s/ ''' %exclude
+
+ if excludes != []:
+ f.write( excludeline)
+ f.write("\n")
+
+ f.close()
+ print(" Writing smb.conf.home")
+ try:
+ f = open("/etc/samba/smb.conf.home","w")
+ except:
+ print(" Couldn't open smb.conf.home")
+ return
+ homelines='''
+[home]
+path = %s
+public = yes
+only guest = yes
+writeable = %s
+printable = no
+force user = mythtv
+force group = mythtv
+create mask = 0755 ''' %(mythhome,shreadonly)
+ f.write(homelines)
+ f.close()
+ print(" " + homelines)
+
+ print("\n Creating samba user mythtv")
+ os.system("(echo mythtv; echo mythtv) | smbpasswd -sa mythtv")
+ print("\n Starting SMB, NMB and avahi services")
+ subprocess.call(["systemctl", "enable", "--now", "smb.service"])
+ subprocess.call(["systemctl", "enable", "--now", "nmb.service"])
+ subprocess.call(["systemctl", "enable", "--now", "avahi-daemon.service"])
+ print("\n Finished setting up samba file sharing")
+
+
+def scan_for_shares():
+ import configparser
+ config = configparser.RawConfigParser()
+ file_list=glob.glob("/etc/storage.d/*.conf")
+ share_list = ['/data/storage/disk0']
+ for conf_file in file_list:
+ try:
+ print(" reading in %s" %conf_file)
+ config.read(conf_file)
+ shareable = config.get('storage','shareable')
+ if shareable == "True" :
+ mp = config.get('storage','mountpoint')
+ share_list.append(mp)
+# if config.get('storage','mmount') == "True" :
+# share_list.append("/myth")
+ except:
+ print(" Couldn't open %s for reading" %conf_file)
+ return share_list
+
+
+def setup_fileshare():
+ setup_samba()
+
+if __name__ == "__main__":
+ setup_fileshare()
diff --git a/linhes/linhes-system/lh_sqlserver.cnf b/linhes/linhes-system/lh_sqlserver.cnf
new file mode 100644
index 0000000..be2d5bd
--- /dev/null
+++ b/linhes/linhes-system/lh_sqlserver.cnf
@@ -0,0 +1,82 @@
+#
+# These groups are read by MariaDB server.
+# Use it for options that only the server (but not clients) should see
+#
+# See the examples of server my.cnf files in /usr/share/mysql/
+#
+
+# this is read by the standalone daemon and embedded servers
+[server]
+
+# this is only for the mysqld standalone daemon
+[mysqld]
+
+#
+# * Galera-related settings
+#
+[galera]
+# Mandatory settings
+#wsrep_on=ON
+#wsrep_provider=
+#wsrep_cluster_address=
+#binlog_format=row
+#default_storage_engine=InnoDB
+#innodb_autoinc_lock_mode=2
+#
+# Allow server to accept connections on all interfaces.
+#
+#bind-address=0.0.0.0
+#
+# Optional setting
+#wsrep_slave_threads=1
+#innodb_flush_log_at_trx_commit=0
+
+# this is only for embedded server
+[embedded]
+
+# This group is only read by MariaDB servers, not by MySQL.
+# If you use the same .cnf file for MySQL and MariaDB,
+# you can put MariaDB-only options here
+[mariadb]
+datadir=/data/srv/mysql
+
+skip-external-locking
+
+net_buffer_length = 8K
+key_buffer_size = 768M
+max_allowed_packet = 1M
+tmp_table_size = 256M
+max_heap_table_size = 256M
+sort_buffer_size = 2M
+read_buffer_size = 2M
+read_rnd_buffer_size = 2M
+myisam_sort_buffer_size = 64M
+thread_cache_size = 8
+query_cache_type = 0
+query_cache_size = 0
+query_cache_limit = 2M
+join_buffer_size = 1M
+ignore-db-dir = lost+found
+table_open_cache = 5000
+open_files_limit = 10000
+
+# Uncomment the following if you are using InnoDB tables
+#innodb_data_home_dir = /var/lib/mysql
+#innodb_data_file_path = ibdata1:10M:autoextend
+#innodb_log_group_home_dir = /var/lib/mysql
+# You can set .._buffer_pool_size up to 50 - 80 %
+# of RAM but beware of setting memory usage too high
+#innodb_buffer_pool_size = 16M
+#innodb_additional_mem_pool_size = 2M
+# Set .._log_file_size to 25 % of buffer pool size
+innodb_log_file_size = 16M
+#innodb_log_buffer_size = 8M
+#innodb_flush_log_at_trx_commit = 1
+#innodb_lock_wait_timeout = 50
+innodb_buffer_pool_instances = 1
+
+# This group is only read by MariaDB-10.8 servers.
+# If you use the same .cnf file for MariaDB of different versions,
+# use this group for options that older servers don't understand
+[mariadb-10.8]
+
diff --git a/linhes/linhes-system/lh_system_backup b/linhes/linhes-system/lh_system_backup
new file mode 100755
index 0000000..b3df6e1
--- /dev/null
+++ b/linhes/linhes-system/lh_system_backup
@@ -0,0 +1,32 @@
+#!/bin/bash
+. /etc/systemconfig
+lh_notify-send "Starting Backup..."
+#alert user the database will not be backed up
+if [ $SystemType != MasterBackend -a $SystemType != Standalone ]
+then
+ lh_notify-send "This is not the MasterBackend.\n Skipping backup of database."
+fi
+
+if [ $SystemType = MasterBackend -o $SystemType = Standalone ]
+then
+ lh_notify-send "Stopping MythBackend..."
+ systemctl stop mythbackend.service
+fi
+
+#do the backup
+lh_notify-send "Starting system backup..."
+lh_system_backup_job 2>&1 > /var/log/system_backup.log
+rc=$?
+if [ $SystemType = MasterBackend -o $SystemType = Standalone ]
+then
+ lh_notify-send "Starting MythBackend..."
+ systemctl start mythbackend.service
+fi
+
+if [ $rc = 0 ]
+then
+ complete_message="Backup completed successfully."
+else
+ complete_message="Backup failed."
+fi
+lh_notify-send "$complete_message"
diff --git a/linhes/linhes-system/lh_system_backup_job b/linhes/linhes-system/lh_system_backup_job
new file mode 100755
index 0000000..6d16596
--- /dev/null
+++ b/linhes/linhes-system/lh_system_backup_job
@@ -0,0 +1,339 @@
+#!/bin/bash
+#process that uses this system backup script
+# - lh_mtc.py
+
+MYTH_RUN_STATUS="1"
+. /etc/profile
+. /etc/systemconfig
+PLEXDIR=/var/lib/plex
+KeepBackups=14
+DATE=`date +%F_%H-%M`
+backup_status=0
+MYTHHOME=`lh_home_check.sh`
+MYTHSHUTDOWN="/usr/bin/mythshutdown"
+
+#find primary backup location
+for dir in /data/storage/disk*
+do
+ if [ $dir == /data/storage/disk0 ]; then
+ continue
+ fi
+ TESTDIR=`readlink $dir`
+ if [ -n $TESTDIR ]; then
+ BACKUPLINK=$dir
+ BACKUPDISK=$TESTDIR
+ BACKUPDIR=$TESTDIR/backup/system_backups
+ break
+ fi
+done
+
+if [ `mountpoint -q $BACKUPDISK 2> /dev/null` ]; then
+ echo " The system doesn't have a second drive. Backup skipped."
+ exit $backup_status
+fi
+
+
+#find secondary backup location
+for dir in /data/storage/disk*
+do
+ if [[ $dir == /data/storage/disk0 || $dir == $BACKUPLINK ]]; then
+ continue
+ fi
+ TESTDIR=`readlink $dir`
+ if [ -n $TESTDIR ]; then
+ SECBACKUPLINK=$dir
+ SECBACKUPDISK=$TESTDIR
+ SECBACKUPDIR=$TESTDIR/backup/system_backups
+ break
+ fi
+done
+
+function lock_myth(){
+ $MYTHSHUTDOWN --lock
+}
+
+function unlock_myth(){
+ $MYTHSHUTDOWN --unlock
+}
+
+function backup_status_check(){
+ if [ $1 -ne 0 ]
+ then
+ backup_status=1
+ fi
+}
+
+
+function backup(){
+
+ echo
+ echo "Starting Backup"
+ echo "Backup Directory: $BACKUPLINK --> $BACKUPDIR"
+ echo "Secondary Backup Directory: $SECBACKUPLINK --> $SECBACKUPDIR"
+ echo
+ mkdir -p $BACKUPDIR/$DATE
+
+ #backup database
+ if [ $SystemType = MasterBackend -o $SystemType = Standalone ]
+ then
+ echo
+ echo "Backup mariadb databases"
+ pacman -Q mysql 2>/dev/null
+ if [ $? = 0 ]
+ then
+ echo " mythconverg (mythtv database)"
+ /usr/bin/mariadb-dump -x mythconverg > $BACKUPDIR/$DATE/mythconverg
+ backup_status_check $?
+
+ #this is everything
+ echo " All databases in one file"
+ /usr/bin/mariadb-dump -x --all-databases > $BACKUPDIR/$DATE/all_databases
+ backup_status_check $?
+ fi
+ fi
+
+ echo "Backup saved settings"
+ if [ -e /usr/MythVantage/templates/settings ]
+ then
+ cp -rp /usr/MythVantage/templates/settings $BACKUPDIR/$DATE/settings
+ backup_status_check $?
+ fi
+
+ echo "Backup etc"
+ cp -rp /etc $BACKUPDIR/$DATE/etc
+
+ echo "Backup Plex Media Server databases and preferences"
+ if [ -e $PLEXDIR/Plex\ Media\ Server/Plug-in\ Support ]
+ then
+ mkdir $BACKUPDIR/$DATE/plex
+ backup_status_check $?
+ fi
+ if [ -e $PLEXDIR/Plex\ Media\ Server/Plug-in\ Support/Databases ]
+ then
+ mkdir -p $BACKUPDIR/$DATE/plex/Databases/
+ backup_status_check $?
+ cp -rp $PLEXDIR/Plex\ Media\ Server/Plug-in\ Support/Databases/*.db $BACKUPDIR/$DATE/plex/Databases/
+ backup_status_check $?
+ cp -rp $PLEXDIR/Plex\ Media\ Server/Plug-in\ Support/Databases/*.db-wal $BACKUPDIR/$DATE/plex/Databases/
+ backup_status_check $?
+ cp -rp $PLEXDIR/Plex\ Media\ Server/Plug-in\ Support/Databases/*.db-shm $BACKUPDIR/$DATE/plex/Databases/
+ backup_status_check $?
+ fi
+ if [ -e $PLEXDIR/Plex\ Media\ Server/Plug-in\ Support/Preferences ]
+ then
+ cp -rp $PLEXDIR/Plex\ Media\ Server/Plug-in\ Support/Preferences $BACKUPDIR/$DATE/plex/Preferences
+ backup_status_check $?
+ fi
+
+ #create default backup_exclude.txt
+ if [ ! -f $MYTHHOME/backup_config/backup_exclude.txt ]
+ then
+ mkdir -p $MYTHHOME/backup_config/
+ touch $MYTHHOME/backup_config/backup_exclude.txt
+ chmod 777 $MYTHHOME/backup_config/backup_exclude.txt
+ fi
+
+ for i in ".mythtv/cache" ".mythtv/themecache" ".mythtv/remotecache" ".mythtv/Cache-myth*" ".cache" "tmp" ".vnc/*log" ".vnc/*pid" ".plexht/userdata/Thumbnails" ".plexht/userdata/ThemeMusicCache" ".kodi/userdata/Thumbnails"
+ do
+ grep -qF "$i" $MYTHHOME/backup_config/backup_exclude.txt
+ rc=$?
+ if [ $rc != 0 ]
+ then
+ echo "$i" >> $MYTHHOME/backup_config/backup_exclude.txt
+ fi
+ done
+
+ echo "Backup home dirs"
+ HOMEDIRS=""
+ tar -I pigz -cf $BACKUPDIR/$DATE/home_dir.tar.gz -X $MYTHHOME/backup_config/backup_exclude.txt $MYTHHOME $HOMEDIRS
+
+ if [ -f $MYTHHOME/backup_config/backup_include.txt ]
+ then
+ echo
+ echo "Backup items from $MYTHHOME/backup_config/backup_include.txt"
+ tar -I pigz -cf $BACKUPDIR/$DATE/other.tar.gz -T $MYTHHOME/backup_config/backup_include.txt
+ backup_status_check $?
+ fi
+
+ echo "Compress backup file"
+ cd $BACKUPDIR
+ tar -I pigz -cf $BACKUPDIR/backup.$DATE.tgz $DATE
+ backup_status_check $?
+ if [ -d $BACKUPDIR/$DATE ]
+ then
+ rm -rf $BACKUPDIR/$DATE
+ fi
+
+ if [ $backup_status -eq 0 ]
+ then
+ BACKUPPATH=$BACKUPDIR/backup.$DATE.tgz
+ else
+ echo "Backup had an error"
+ mkdir $BACKUPDIR/errored_backups
+ mv $BACKUPDIR/backup.$DATE.tgz $BACKUPDIR/errored_backups/backup.$DATE.tgz
+ BACKUPPATH=$BACKUPDIR/errored_backups/backup.$DATE.tgz
+ fi
+
+ if [ -f /home/xymon/server/ext/hbnotes.py ]
+ then
+ /home/xymon/server/ext/hbnotes.py
+ chown nobody:nobody /data/srv/httpd/htdocs/hobbit/notes/* 2> /dev/null >/dev/null
+ fi
+ echo
+ echo "Created backup file:"
+ echo " $BACKUPPATH"
+}
+
+function update_backup_status(){
+ echo
+ # Add Last backup status to menu item
+ #if description not in the backup xml file, add it
+ if [ $rc=0 ]
+ then
+ COMPLETE_MSG="Last backup completed `date '+%D %-I:%M %p'`"
+ else
+ COMPLETE_MSG="Last backup FAILED `date '+%D %-I:%M %p'`"
+ fi
+ echo "Updating menu with:"
+ echo " $COMPLETE_MSG"
+ xmlfile="/usr/share/mythtv/themes/defaultmenu/mythbackup.xml"
+
+ grep -q "<description>" $xmlfile >/dev/null
+ desc_check=$?
+
+ if [ $desc_check = 0 ]
+ then
+ sed -i "0,/<description\>.*\<description\>/s||\<description\>$COMPLETE_MSG<\/description|" $xmlfile
+ #sed -i "0,/\<description\>.*\<description\>/s//\<description\>$COMPLETE_MSG\<\/description/" $xmlfile
+ else
+ sed -i " /NONE/ i\ \<description\>$COMPLETE_MSG\<\/description\>" $xmlfile
+ fi
+}
+
+function remove_old_backups(){
+ #remove old backups
+ NumBackups=`ls $BACKUPDIR/backup*.tgz|wc -l`
+ if [[ $NumBackups -gt $KeepBackups ]]; then
+ numdel=$(($NumBackups-$KeepBackups))
+ rm -f `ls $BACKUPDIR/backup*.tgz -tr1|head -$numdel`
+ fi
+}
+
+
+function remote_backup(){
+ echo
+ echo "Remote backup"
+ #Remote copy
+ if [ x$RemoteBackup = x1 ]
+ then
+ localRemoteCheck=`echo $RemoteBackupDir | cut -d: -f1`
+ if [ x$localRemoteCheck = xdir ]
+ then
+ localRemotedir=`echo $RemoteBackupDir | cut -d: -f2`
+ echo " copying $BACKUPDIR/backup.$DATE.tgz to $localRemotedir "
+ cp $BACKUPDIR/backup.$DATE.tgz $localRemotedir
+ else
+ /usr/bin/func ${RemoteBackupDir} ping| grep -q "FAILED"
+ rc=$?
+ if [ $rc = 0 ]
+ then
+ #this is here to mark failed copy of the backup.
+ #There is a cron.hourly job that will attempt to retransfer the file
+ echo " Remote backup failed to ${RemoteBackupDir}"
+ echo backup.$DATE.tgz >> $BACKUPDIR/remote_backup_failed.txt
+ else
+ echo " copying $BACKUPDIR/backup.$DATE.tgz to ${RemoteBackupDir}:$BACKUPDIR/MBE_$DATE.tgz"
+ /usr/bin/func ${RemoteBackupDir} copyfile -f $BACKUPDIR/backup.$DATE.tgz --remotepath $BACKUPDIR/MBE_$DATE.tgz
+ fi
+ fi
+ else #do local copy to SECBACKUPLINK
+ echo " Remote backup is not enabled, copying backup to another drive on this system."
+
+ if [ -n "$SECBACKUPDISK" ]; then
+ SECBACKUP=$SECBACKUPDISK/backup
+ if [ ! `mountpoint -q $SECBACKUPDISK 2> /dev/null` ]; then
+ if [ ! -d "$SECBACKUPDIR" ]; then
+ mkdir -p -m 775 $SECBACKUPDIR
+ echo " Created $SECBACKUPDIR"
+ chown mythtv:users $SECBACKUPDIR
+ fi
+ echo " Copying system backups to $SECBACKUPDIR"
+ rsync -au --delete $BACKUPDIR/ $SECBACKUPDIR/
+ else
+ echo " $SECBACKUPDISK isn't mounted."
+ fi
+ else
+ echo " Could not find another drive on this system."
+ fi
+ fi
+}
+
+function remote_transfer(){
+ transfer_file=${1}
+ echo $transfer_file
+ /usr/bin/func ${RemoteBackupDir} ping| grep -q "FAILED"
+ rc=$?
+ if [ $rc = 0 ]
+ then
+ #this is here to mark a failed copy of the backup.
+ #There is a cron.hourly job that will attempt to retransfer the file
+ echo " Remote backup failed to ${RemoteBackupDir}"
+ echo $transfer_file >> $BACKUPDIR/remote_backup_failed.txt
+ else
+ echo " copying $BACKUPDIR/$transfer_file to ${RemoteBackupDir}:$BACKUPDIR/MBE_$transfer_file"
+ /usr/bin/func ${RemoteBackupDir} copyfile -f $BACKUPDIR/$transfer_file --remotepath $BACKUPDIR/MBE_$transfer_file
+ fi
+}
+
+function add_link(){
+ if [ -f $BACKUPDIR/remote_backup_failed.txt ]
+ then
+ RETRYFILE="/etc/cron.hourly/lh_backup_retry.sh"
+ echo "#!/bin/bash" > $RETRYFILE
+ echo "#This file was autogenerated and will be removed by lh_system_backup_job" >> $RETRYFILE
+ echo "MYTH_RUN_STATUS=1">> $RETRYFILE
+ echo ". /etc/profile">> $RETRYFILE
+ echo "lh_system_backup_job retry">> $RETRYFILE
+ chmod 755 $RETRYFILE
+ fi
+}
+
+function remove_link(){
+ RETRYFILE="/etc/cron.hourly/lh_backup_retry.sh"
+ if [ ! -f $BACKUPDIR/remote_backup_failed.txt ]
+ then
+ rm -f $RETRYFILE
+ fi
+}
+
+
+#------------------------------------
+lock_myth
+if [ "x$1" = "x" ]
+then
+ backup
+ #update_backup_status
+ #only remove old backups if there was no problems
+ if [ $backup_status -eq 0 ]
+ then
+ remove_old_backups
+ fi
+ remote_backup
+ add_link
+else
+ #this is where we attempt to transfer again as part of the cronjob
+ if [ -f $BACKUPDIR/remote_backup_failed.txt ]
+ then
+ mv -f $BACKUPDIR/remote_backup_failed.txt /tmp
+ while read line
+ do
+ echo $line
+ remote_transfer $line
+ done < /tmp/remote_backup_failed.txt
+ rm -f /tmp/remote_backup_failed.txt
+ fi
+ remove_link
+fi
+unlock_myth
+echo $backup_status > /var/run/systembackup.rc
+exit $backup_status
diff --git a/linhes/linhes-system/lh_system_start.sh b/linhes/linhes-system/lh_system_start.sh
new file mode 100755
index 0000000..3cbf5dc
--- /dev/null
+++ b/linhes/linhes-system/lh_system_start.sh
@@ -0,0 +1,159 @@
+#!/bin/bash
+
+. /etc/profile
+#. /etc/systemconfig
+
+HOSTNAME=`/usr/bin/hostnamectl hostname`
+
+function msg(){
+ /usr/bin/lh_notify-send --app-name="LinHES" --icon=dialog-information "$1" "$2"
+}
+
+function check_installer_user(){
+ if [[ "$(whoami)" = 'km' ]]; then
+ echo "Running lh_system_start as installer user km. Exiting."
+ exit 1
+ fi
+}
+
+function applyUIsettings(){
+ /usr/bin/lh_apply_UI_settings.sh
+ msg "Welcome to LinHES 9!"
+}
+
+function x11vnc_setup(){
+ konsole -e /bin/bash -i -c "echo 'Create VNC password.' && x11vnc --storepasswd"
+ mkdir -p ~/.vnc
+ touch ~/.vnc/x11vnc.log
+ sudo /usr/bin/systemctl enable --now x11vnc.service
+}
+
+function bashrc_setup(){
+ if ! grep -q 'alias rscp=' ~/.bashrc; then
+ echo -e "\nalias rscp='rsync -a --info=progress2'" >> ~/.bashrc
+ fi
+ if ! grep -q 'alias rsmv=' ~/.bashrc; then
+ echo -e "alias rsmv='rsync -a --info=progress2 --remove-source-files'" >> ~/.bashrc
+ fi
+}
+
+function nanorc_setup(){
+ sudo sed -i 's/# set tabsize.*/set tabsize 4/' /etc/nanorc
+ sudo sed -i 's/# set tabstospaces.*/set tabstospaces/' /etc/nanorc
+ sudo sed -i 's/# include "\/usr\/share\/nano\/\*.nanorc"/include "\/usr\/share\/nano\/\*.nanorc"/' /etc/nanorc
+}
+
+function storage_scan(){
+ sudo add_storage.py --report > /dev/null
+ if [ -e /tmp/scan_report ]; then
+ msg "New Storage Found" "Run add_storage.py for details."
+ fi
+}
+
+function install_lh_apps(){
+ #install programs that are not needed on the iso
+ #check network connection
+ netwait=0
+ while ! timeout 1 nc -zw1 1.1.1.1 443; do
+ [ $netwait -gt 12 ] && msg "Could not install apps. Check internet connection. Cancelling Setup." && exit 1
+ msg "Waiting for internet connectivity..."
+ ((netwait++))
+ sleep 5
+ done
+ msg "Installing apps."
+ konsole -e /bin/bash -i -c "sudo pacman -Syyy --noconfirm archlinux-keyring && sudo pacman -Syyy --noconfirm mythtv mythplugins-mytharchive mythplugins-mythmusic mythplugins-mythweb && flatpak install tv.plex.PlexHTPC --noninteractive --assumeyes && sudo flatpak override tv.plex.PlexHTPC --filesystem=/run/lirc/lircd"
+ status=$?
+ [ $status -eq 1 ] && msg "Could not install apps. Check internet connection. Cancelling Setup." && exit 1
+ gen_lib_xml.py
+}
+
+function sql_setup(){
+ sudo mkdir -p /data/srv/mysql
+ sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/data/srv/mysql
+ sudo systemctl enable --now mariadb.service
+ mysql_tzinfo_to_sql /usr/share/zoneinfo | sudo mysql -u root mysql
+ sudo mariadb -u root < /usr/share/linhes/templates/db/permissions.sql
+ mythtv-setup -O theme=LinHES
+ sed -e "s/apheleia/${HOSTNAME}/g" /usr/share/linhes/templates/db/custom.sql > /tmp/custom.sql.fixed
+ sudo mariadb -u root mythconverg < /tmp/custom.sql.fixed
+}
+
+function localweb_setup(){
+ konsole -e /bin/bash -i -c "sudo pacman -Syyy --noconfirm linhes-web"
+ sudo cp /etc/lighttpd/lighttpd.conf /etc/lighttpd/lighttpd.conf.orig
+ sudo cp /usr/share/linhes/templates/lighttpd.conf.template /etc/lighttpd/lighttpd.conf
+ sudo systemctl enable --now lighttpd.service
+}
+
+function first_configure(){
+ if [ ! -f ~/.config/lh_configured ]; then
+ msg "New install of LinHES. Starting setup."
+ install_lh_apps
+ nanorc_setup
+ bashrc_setup
+ x11vnc_setup
+ if [ -f /etc/systemconfig ]; then
+ SystemType=$(grep SystemType= /etc/systemconfig | cut -d '"' -f 2)
+ else
+ SystemType=$(kdialog --title "LinHES System Type" --combobox "Select the LinHES System Type: " "MasterBackend" "FrontendOnly" "DesktopOnly" --default "MasterBackend")
+ echo "SystemType=\"$SystemType\"" | sudo tee /etc/systemconfig
+ fi
+ #apply settings for specific system types
+ msg "Setup as $SystemType"
+ if [ $SystemType = "MasterBackend" ]; then
+ # create media directory structure
+ sudo mkdir -p /data/storage/disk0
+ sudo create_media_dirs.sh /data/storage/disk0
+ sudo ln -s /data/storage/disk0/media/ /myth
+ # setup DB
+ sql_setup
+ # run mythtv-setup
+ # need to run twice for default db install/upgrade
+ mythtv-setup
+ sudo systemctl enable --now mythbackend.service
+ # run mythfilldatabase
+ #nice -n 19 mythfilldatabase --quiet &
+ #msg "Guide data is being loaded." "Until this completes some shows will appear as unknown in the program guide."
+ localweb_setup
+ elif [ $SystemType = "FrontendOnly" ]; then
+ msg "Frontend Only"
+ #Frontend_only cmds
+ # create media directory structure
+ sudo mkdir -p /data/storage/disk0
+ sudo create_media_dirs.sh /data/storage/disk0
+ elif [ $SystemType = "DesktopOnly" ]; then
+ msg "Desktop Only"
+ #Frontend_only cmds
+ # create media directory structure
+ sudo mkdir -p /data/storage/disk0
+ sudo create_media_dirs.sh /data/storage/disk0
+ touch ~/.config/lh_dontrunmythfrontend
+ fi
+ touch ~/.config/lh_configured
+ fi
+}
+
+function start_user_apps() {
+ if [ -f ~/.config/lh_startuserapps ]; then
+ msg "Starting User Apps..."
+ source ~/.config/lh_startuserapps
+ fi
+}
+
+function start_myth() {
+ STARTCMD="/usr/bin/mythfrontend --syslog local6 --quiet"
+ if [ ! -f ~/.config/lh_dontrunmythfrontend ]; then
+ msg "Starting MythFrontend..."
+ $STARTCMD 2>&1 &
+ fi
+}
+
+#-------MAIN-------
+check_installer_user
+#apply settings for all system types
+applyUIsettings
+first_configure
+/usr/bin/enableIRWake.sh &
+storage_scan
+start_user_apps
+start_myth
diff --git a/linhes/linhes-system/lh_system_start.sh.desktop b/linhes/linhes-system/lh_system_start.sh.desktop
new file mode 100644
index 0000000..6dc353e
--- /dev/null
+++ b/linhes/linhes-system/lh_system_start.sh.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Exec=/usr/bin/lh_system_start.sh
+Icon=dialog-scripts
+Name=LinHES_Startup
+Path=
+Type=Application
+X-KDE-AutostartScript=true
diff --git a/linhes/linhes-system/linhes-profile.sh b/linhes/linhes-system/linhes-profile.sh
new file mode 100644
index 0000000..df98315
--- /dev/null
+++ b/linhes/linhes-system/linhes-profile.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+#this enables airplay support
+export MYTHTV_AIRPLAY="1"
+
+if [ -f /etc/systemconfig ]; then
+ SystemType=$(grep SystemType= /etc/systemconfig | cut -d '"' -f 2)
+fi
+if [ ! $SystemType = "FrontendOnly" ]; then
+ if [ x$MYTH_RUN_STATUS = x ]; then
+ MYTHCONFDIR=/usr/share/mythtv /usr/bin/lh_myth_status.py
+ fi
+ MYTH_RUN_STATUS="1"
+fi
diff --git a/linhes/linhes-system/misc_recent_recordings.pl b/linhes/linhes-system/misc_recent_recordings.pl
new file mode 100755
index 0000000..92dda8a
--- /dev/null
+++ b/linhes/linhes-system/misc_recent_recordings.pl
@@ -0,0 +1,194 @@
+#!/usr/bin/perl -w
+#
+# Outputs information about the most-recently-recorded shows.
+#
+# Automatically detects database settings.
+#
+
+# Includes
+ use DBI;
+ use Getopt::Long;
+ use MythTV;
+
+# Some variables we'll use here
+ our ($num_recordings, $live, $heading, $plain_text, $text_format, $usage);
+ our ($hours, $minutes, $seconds);
+ our ($dnum_recordings, $dheading, $dtext_format);
+ our ($dhours, $dminutes, $dseconds);
+ our ($status_text_format, $status_value_format);
+ our ($dstatus_text_format, $dstatus_value_format);
+
+# Default number of recent recordings to show
+ $dnum_recordings = 5;
+# Default period in which to show recordings
+ $dhours = -1;
+ $dminutes = -1;
+ $dseconds = -1;
+# Default status output heading
+ $dheading='Recent Recordings:\n';
+# Default format of plain-text output
+ $dtext_format='%n/%j, %g:%i %A - %cc\n%T - %S\n%R\n\n';
+# Default format of status output display text
+ $dstatus_text_format= '<a href="javascript:void(0)">%n/%j %g:%i %A - %cc - %T - %S<br />'.
+ '<span><strong>%T</strong> %n/%j, %g:%i %A<br />'.
+ '<em>%S</em><br /><br />%R<br /></span></a><hr />';
+# Default format of status output value
+ $dstatus_value_format = '%n/%j, %g:%i %A - %T - %S';
+
+# Provide default values for GetOptions
+ $num_recordings = $dnum_recordings;
+ $hours = $dhours;
+ $minutes = $dminutes;
+ $seconds = $dseconds;
+ $heading = $dheading;
+ $text_format = $dtext_format;
+ $status_text_format = $dstatus_text_format;
+ $status_value_format = $dstatus_value_format;
+
+# Load the cli options
+ GetOptions('num_recordings|recordings=s' => \$num_recordings,
+ 'hours|o=i' => \$hours,
+ 'minutes=i' => \$minutes,
+ 'seconds|e=i' => \$seconds,
+ 'live' => \$live,
+ 'heading=s' => \$heading,
+ 'plain_text' => \$plain_text,
+ 'text_format=s' => \$text_format,
+ 'status_text_format=s' => \$status_text_format,
+ 'status_value_format=s' => \$status_value_format,
+ 'usage|help' => \$usage
+ );
+
+# Print usage
+ if ($usage) {
+ print <<EOF;
+$0 usage:
+
+options:
+
+--recordings [number of recordings]
+
+ Outputs information on the last [number of recordings] shows recorded by
+ MythTV. To output information on all recordings, specify -1.
+
+ default: $dnum_recordings
+
+--hours [number of hours]
+
+ Outputs information on recordings that occurred within [number of hours].
+ This option may be specified in conjunction with --minutes and --seconds.
+ To output information on all matching recordings regardless of start time,
+ specify -1 for --hours, --minutes, and --seconds.
+
+ default: $dhours
+
+--minutes [number of minutes]
+
+ Outputs information on recordings that occurred within [number of minutes].
+ This option may be specified in conjunction with --hours and --seconds.
+ To output information on all matching recordings regardless of start time,
+ specify -1 for --hours, --minutes, and --seconds.
+
+ default: $dminutes
+
+--seconds [number of seconds]
+
+ Outputs information on recordings that occurred within [number of seconds].
+ This option may be specified in conjunction with --hours and --minutes.
+ To output information on all matching recordings regardless of start time,
+ specify -1 for --hours, --minutes, and --seconds.
+
+ default: $dseconds
+
+--live
+ Include information on recent LiveTV recordings.
+
+--heading [heading]
+ Output the [heading] before printing information about recordings.
+
+ default: \'$dheading\'
+
+--plain_text
+ Output information in plain text format (i.e. for inclusion in an e-mail
+ notification).
+
+--text_format [format]
+ Use the provided [format] to display information on the recordings. The
+ format should use the same format specifiers used by mythlink.pl, but
+ may also use \\r and/or \\n for line breaks. This option is ignored
+ if --plain_text is not used.
+
+ default: \'$dtext_format\'
+
+--help
+
+ Show this help text.
+
+EOF
+ exit;
+ }
+
+# Determine the period of interest
+ my $now = time();
+ my $start_after = $now;
+ $start_after = $start_after - ($hours * 3600) if ($hours > 0);
+ $start_after = $start_after - ($minutes * 60) if ($minutes > 0);
+ $start_after = $start_after - $seconds if ($seconds > 0);
+ $start_after = 0 if (!($start_after < $now));
+
+# Fix the heading.
+ if (defined($plain_text)) {
+ $heading =~ s/\\r/\r/g;
+ $heading =~ s/\\n/\n/g;
+ }
+ else {
+ # Remove line break format specifiers from heading for status output
+ $heading =~ s/(\\r|\\n)//g;
+ }
+
+# Connect to mythbackend
+ my $Myth = new MythTV();
+
+# Get the list of recordings
+ my $count = 0;
+ my %rows = $Myth->backend_rows('QUERY_RECORDINGS Delete');
+ our $show;
+ foreach my $row (@{$rows{'rows'}}) {
+ last unless (($count < $num_recordings) || ($num_recordings < 0));
+ $show = new MythTV::Program(@$row);
+ # Skip LiveTV recordings?
+ next unless (defined($live) || $show->{'recgroup'} ne 'LiveTV');
+ # Within the period of interest?
+ last if (($start_after) && ($show->{'recstartts'} < $start_after));
+ # Print the recording information in the desired format
+ if (defined($plain_text)) {
+ text_print($count);
+ }
+ else {
+ status_print($count);
+ }
+ $count++;
+ }
+
+# Print the output for use in the backend status page.
+ sub status_print {
+ my $count = shift;
+ my $text = $show->format_name($status_text_format, ' ', ' ', 1, 0 ,1);
+ my $value = $show->format_name($status_value_format, ' ', ' ',
+ 1, 0 ,1);
+ print("$heading<div class=\"schedule\">") if ($count == 0);
+ print("$text");
+ print("</div>") if ($count == ($num_recordings - 1));
+ print("[]:[]recording$count");
+ print("[]:[]$value\n");
+ }
+
+# Print the output in plain text format
+ sub text_print {
+ my $count = shift;
+ my $text = $show->format_name($text_format, ' ', ' ', 1, 0 ,1);
+ $text =~ s/\\r/\r/g;
+ $text =~ s/\\n/\n/g;
+ print("$heading") if ($count == 0);
+ print("$text");
+ }
diff --git a/linhes/linhes-system/misc_status_config.py b/linhes/linhes-system/misc_status_config.py
new file mode 100755
index 0000000..b025886
--- /dev/null
+++ b/linhes/linhes-system/misc_status_config.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+# This script will configure the myth db to use the misc_status_info scripts
+
+from socket import gethostname
+from MythTV import MythDB
+mythdb = MythDB()
+localhostname = gethostname()
+import sys
+
+# Function to set db setting. This setting is set in mythtv-setup.
+def dbSettingChange():
+ if mythdb.settings[localhostname].MiscStatusScript == u'':
+ mythdb.settings[localhostname].MiscStatusScript = u'/usr/bin/misc_status_info.sh'
+ print 'The MythTV database setting MiscStatusScript was updated to /usr/bin/misc_status_info.sh.'
+ else:
+ print 'The MythTV database setting MiscStatusScript is already set and will not be updated.'
+ return
+
+#taken from systemconfig.py
+#this is how you populate the dict
+systemconfig = {}
+file_name = "/etc/systemconfig"
+try:
+ config_file = open(file_name)
+except:
+ print file_name + " could not be opened"
+ sys.exit(1)
+
+for line in config_file:
+ line = line.strip()
+ if line and line[0] is not "#" and line[-1] is not "=":
+ var, val = line.rsplit("=", 1)
+ val = val.strip('"')
+ systemconfig[var.strip()] = val.strip()
+
+#this is how you reference a value from mv_hostype.py
+if (systemconfig.get("SystemType") == "Standalone"):
+ dbSettingChange()
+elif systemconfig.get("SystemType") == "MasterBackend":
+ dbSettingChange()
diff --git a/linhes/linhes-system/misc_status_info.sh b/linhes/linhes-system/misc_status_info.sh
new file mode 100755
index 0000000..3061a08
--- /dev/null
+++ b/linhes/linhes-system/misc_status_info.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# Show all upcoming conflicts
+/usr/bin/misc_upcoming_recordings.pl --recordings -1 \
+ --no_show_scheduled \
+ --heading '<h3>Recording Conflicts</h3>' \
+ --no_conflicts_message '<h3>No Recording Conflicts</h3>'
+
+# Show all the shows recorded today
+/usr/bin/misc_recent_recordings.pl --recordings=-1 --hours=24 \
+ --heading '<h3>Shows Recorded In The Last 24 Hours</h3>'
+
+# Get Encoder that was used for recordings from the backend log
+firstrun=1
+loglist=`find /var/log/ -name *_mythbackend*.log*`
+for i in `ls -t $loglist`
+do
+ if [ $firstrun -eq 1 ]; then
+ echo "<h3>Encoder Information</h3><div class=\"schedule\""
+ firstrun=0
+ fi
+ /usr/bin/misc_which_recorder.pl --noheader $i
+done
diff --git a/linhes/linhes-system/misc_upcoming_recordings.pl b/linhes/linhes-system/misc_upcoming_recordings.pl
new file mode 100755
index 0000000..26ec1b4
--- /dev/null
+++ b/linhes/linhes-system/misc_upcoming_recordings.pl
@@ -0,0 +1,334 @@
+#!/usr/bin/perl -w
+#
+# Provides notification of upcoming recordings.
+#
+# Automatically detects database settings.
+#
+
+# Includes
+ use DBI;
+ use Getopt::Long;
+ use MythTV;
+
+# Some variables we'll use here
+ our ($num_recordings, $heading, $plain_text, $text_format, $usage);
+ our ($hours, $minutes, $seconds, $no_conflicts_message);
+ our ($scheduled, $duplicates, $deactivated, $conflicts);
+ our ($dnum_recordings, $dheading, $dtext_format);
+ our ($dhours, $dminutes, $dseconds, $dno_conflicts_message);
+ our ($dscheduled, $dduplicates, $ddeactivated, $dconflicts);
+ our ($status_text_format, $status_value_format);
+ our ($dstatus_text_format, $dstatus_value_format);
+
+# Default number of upcoming recordings to show
+ $dnum_recordings = 5;
+# Default period in which to show recordings
+ $dhours = -1;
+ $dminutes = -1;
+ $dseconds = -1;
+# Default recording status types to show
+ $dscheduled = 1;
+ $dduplicates = 0;
+ $ddeactivated = 0;
+ $dconflicts = 1;
+# Default status output heading
+ $dheading='Upcoming Recordings:\n';
+# Default format of plain-text output
+ $dtext_format='%rs\n%n/%j, %g:%i %A - %cc\n%T - %S\n%R\n\n';
+# Default "no conflicts" message
+ $dno_conflicts_message='No conflicts.\n';
+# Default format of status output display text
+ $dstatus_text_format= '<a href="javascript:void(0)">%rs - %n/%j %g:%i %A - %cc - '.
+ '%T - %S<br />'.
+ '<span><strong>%T</strong> %n/%j, %g:%i %A<br />'.
+ '<em>%S</em><br /><br />%R<br /></span></a><hr />';
+# Default format of status output value
+ $dstatus_value_format = '%n/%j %g:%i %A - %T - %S';
+
+# Provide default values for GetOptions
+ $num_recordings = $dnum_recordings;
+ $hours = $dhours;
+ $minutes = $dminutes;
+ $seconds = $dseconds;
+ $scheduled = $dscheduled;
+ $duplicates = $dduplicates;
+ $deactivated = $ddeactivated;
+ $conflicts = $dconflicts;
+ $heading = $dheading;
+ $text_format = $dtext_format;
+ $no_conflicts_message = $dno_conflicts_message;
+ $status_text_format = $dstatus_text_format;
+ $status_value_format = $dstatus_value_format;
+
+# Load the cli options
+ GetOptions('num_recordings|recordings=s' => \$num_recordings,
+ 'hours|o=i' => \$hours,
+ 'minutes=i' => \$minutes,
+ 'seconds|s=i' => \$seconds,
+ 'show_scheduled|_show_scheduled|scheduled|_scheduled|e!'
+ => \$scheduled,
+ 'show_duplicates|_show_duplicates|duplicates|_duplicates|p!'
+ => \$duplicates,
+ 'show_deactivated|_show_deactivated|deactivated|_deactivated|v!'
+ => \$deactivated,
+ 'show_conflicts|_show_conflicts|conflicts|_conflicts!'
+ => \$conflicts,
+ 'heading=s' => \$heading,
+ 'plain_text' => \$plain_text,
+ 'text_format=s' => \$text_format,
+ 'no_conflicts_message=s' => \$no_conflicts_message,
+ 'status_text_format=s' => \$status_text_format,
+ 'status_value_format=s' => \$status_value_format,
+ 'usage|help' => \$usage
+ );
+
+# Print usage
+ if ($usage) {
+ # Make default "--show_*" options readable
+ $dscheduled = ($dscheduled ? '--show_scheduled' :
+ '--no_show_scheduled');
+ $dduplicates = ($dduplicates ? '--show_duplicates' :
+ '--no_show_duplicates');
+ $ddeactivated = ($ddeactivated ? '--show_deactivated' :
+ '--no_show_deactivated');
+ $dconflicts = ($dconflicts ? '--show_conflicts' :
+ '--no_show_conflicts');
+ print <<EOF;
+$0 usage:
+
+options:
+
+--recordings [number of recordings]
+
+ Outputs information on the next [number of recordings] shows to be recorded
+ by MythTV and that match the criteria specified for --scheduled,
+ --duplicates, --deactivated, and --conflicts. To output information on all
+ matching recordings, specify -1.
+
+ default: $dnum_recordings
+
+--hours [number of hours]
+
+ Outputs information on recordings starting in the next [number of hours]
+ and that match the criteria specified for --scheduled, --duplicates,
+ --deactivated, and --conflicts. This option may be specified in
+ conjunction with --minutes and --seconds. To output information on all
+ matching recordings regardless of start time, specify -1 for --hours,
+ --minutes, and --seconds.
+
+ default: $dhours
+
+--minutes [number of minutes]
+
+ Outputs information on recordings starting in the next [number of minutes]
+ and that match the criteria specified for --scheduled, --duplicates,
+ --deactivated, and --conflicts. This option may be specified in
+ conjunction with --hours and --seconds. To output information on all
+ matching recordings regardless of start time, specify -1 for --hours,
+ --minutes, and --seconds.
+
+ default: $dminutes
+
+--seconds [number of seconds]
+
+ Outputs information on recordings starting in the next [number of seconds]
+ and that match the criteria specified for --scheduled, --duplicates,
+ --deactivated, and --conflicts. This option may be specified in
+ conjunction with --hours and --minutes. To output information on all
+ matching recordings regardless of start time, specify -1 for --hours,
+ --minutes, and --seconds.
+
+ default: $dseconds
+
+--show_scheduled|--no_show_scheduled
+
+ Outputs information about scheduled recordings. Scheduled recordings are
+ those that MythTV plans to actually record.
+
+ default: $dscheduled
+
+--show_duplicates|--no_show_duplicates
+
+ Outputs information about duplicate recordings. Duplicate recordings are
+ those that will not be recorded because of the specified duplicate matching
+ policy for the rule.
+
+ default: $dduplicates
+
+--show_deactivated|--no_show_deactivated
+
+ Outputs information about deactivated recordings. Deactivated recordings
+ are those that MythTV will not record because the schedule is inactive,
+ because the showing was set to never record, because the show is being
+ recorded in an earlier or later showing, because there are too many
+ recordings or not enough disk space to allow the recording, or because
+ the show you\'ve specified for recording is not listed in the timeslot
+ specified.
+
+ default: $ddeactivated
+
+--show_conflicts|--no_show_conflicts
+
+ Outputs information about conflicts (those shows that MythTV cannot record
+ because of other higher-priority scheduled recordings).
+
+ default: $dconflicts
+
+--heading [heading]
+ Output the [heading] before printing information about recordings.
+
+ default: \'$dheading\'
+
+--plain_text
+ Output information in plain text format (i.e. for inclusion in an e-mail
+ notification).
+
+--text_format [format]
+ Use the provided [format] to display information on the recordings. The
+ format should use the same format specifiers used by mythlink.pl, but
+ may also use \\r and/or \\n for line breaks and %rs for recording status.
+ This option is ignored if --plain_text is not used.
+
+ default: \'$dtext_format\'
+
+--no_conflicts_message [message]
+ Use the provided [message] to specify there are no conflicts. This option
+ is used when only information about conflicts is requested and there are
+ no conflicts. I.e. it is only used with the combination of show_*
+ options --show_conflicts, --no_show_scheduled, --no_show_deactivated,
+ and --no_show_duplicates .
+
+ default: \'$dno_conflicts_message\'
+
+--help
+
+ Show this help text.
+
+EOF
+ exit;
+ }
+
+# Determine the period of interest
+ my $now = time();
+ my $start_before = $now;
+ $start_before = $start_before + ($hours * 3600) if ($hours > 0);
+ $start_before = $start_before + ($minutes * 60) if ($minutes > 0);
+ $start_before = $start_before + $seconds if ($seconds > 0);
+ $start_before = 0 if (!($start_before > $now));
+
+# Fix the heading.
+ if (defined($plain_text)) {
+ $heading =~ s/\\r/\r/g;
+ $heading =~ s/\\n/\n/g;
+ }
+ else {
+ # Remove line break format specifiers from heading for status output
+ $heading =~ s/(\\r|\\n)//g;
+ }
+
+# Connect to mythbackend
+ my $Myth = new MythTV();
+
+# Get the list of recordings
+ my $count = 0;
+ my %rows = $Myth->backend_rows('QUERY_GETALLPENDING', 2);
+ my $has_conflicts = $rows{'offset'}[0];
+ if ((!$has_conflicts) &&
+ (($conflicts) &&
+ (!(($scheduled) || ($duplicates) || ($deactivated))))) {
+ $no_conflicts_message =~ s/\\r/\r/g;
+ $no_conflicts_message =~ s/\\n/\n/g;
+ print "$no_conflicts_message";
+ exit 0;
+ }
+ my $num_scheduled = $rows{'offset'}[1];
+ our $show;
+ foreach my $row (@{$rows{'rows'}}) {
+ last unless (($count < $num_recordings) || ($num_recordings < 0));
+ $show = new MythTV::Program(@$row);
+ last if (($start_before) && ($show->{'recstartts'} > $start_before));
+ next if ((!$scheduled) && (is_scheduled($show->{'recstatus'})));
+ next if ((!$duplicates) && (is_duplicate($show->{'recstatus'})));
+ next if ((!$deactivated) && (is_deactivated($show->{'recstatus'})));
+ next if ((!$conflicts) && (is_conflict($show->{'recstatus'})));
+
+ # Print the recording information in the desired format
+ if (defined($plain_text)) {
+ text_print($count);
+ }
+ else {
+ status_print($count);
+ }
+ $count++;
+ }
+
+# Returns true if the show is scheduled to record
+ sub is_scheduled {
+ my $recstatus = (shift() or 0);
+ return (($MythTV::recstatus_willrecord == $recstatus) ||
+ ($MythTV::recstatus_recorded == $recstatus) ||
+ ($MythTV::recstatus_recording == $recstatus));
+ }
+
+# Returns true if the show is a duplicate
+ sub is_duplicate {
+ my $recstatus = (shift() or 0);
+ return (($MythTV::recstatus_repeat == $recstatus) ||
+ ($MythTV::recstatus_previousrecording == $recstatus) ||
+ ($MythTV::recstatus_currentrecording == $recstatus));
+ }
+
+# Returns true if the recording is deactivated
+ sub is_deactivated {
+ my $recstatus = (shift() or 0);
+ return (($MythTV::recstatus_inactive == $recstatus) ||
+ ($MythTV::recstatus_toomanyrecordings == $recstatus) ||
+ ($MythTV::recstatus_cancelled == $recstatus) ||
+ ($MythTV::recstatus_deleted == $recstatus) ||
+ ($MythTV::recstatus_aborted == $recstatus) ||
+ ($MythTV::recstatus_notlisted == $recstatus) ||
+ ($MythTV::recstatus_dontrecord == $recstatus) ||
+ ($MythTV::recstatus_lowdiskspace == $recstatus) ||
+ ($MythTV::recstatus_tunerbusy == $recstatus) ||
+ ($MythTV::recstatus_neverrecord == $recstatus) ||
+ ($MythTV::recstatus_earliershowing == $recstatus) ||
+ ($MythTV::recstatus_latershowing == $recstatus));
+ }
+
+# Returns true if the show cannot be recorded due to a conflict
+ sub is_conflict {
+ my $recstatus = (shift() or 0);
+ return ($MythTV::recstatus_conflict == $recstatus);
+ }
+
+# Print the output for use in the backend status page.
+ sub status_print {
+ my $count = shift;
+ my $text = $show->format_name($status_text_format, ' ', ' ', 1, 0 ,1);
+ {
+ no warnings 'uninitialized';
+ $text =~ s/%rs/$MythTV::RecStatus_Types{$show->{'recstatus'}}/g;
+ }
+ my $value = $show->format_name($status_value_format, ' ', ' ',
+ 1, 0 ,1);
+ $value =~ s/%rs/$MythTV::RecStatus_Types{$show->{'recstatus'}}/g;
+ print("$heading<div class=\"schedule\">") if ($count == 0);
+ print("$text");
+ print("</div>") if ($count == ($num_recordings - 1));
+ print("[]:[]recording$count");
+ print("[]:[]$value\n");
+ }
+
+# Print the output in plain text format
+ sub text_print {
+ my $count = shift;
+ my $text = $show->format_name($text_format, ' ', ' ', 1, 0 ,1);
+ {
+ no warnings 'uninitialized';
+ $text =~ s/%rs/$MythTV::RecStatus_Types{$show->{'recstatus'}}/g;
+ }
+ $text =~ s/\\r/\r/g;
+ $text =~ s/\\n/\n/g;
+ print("$heading") if ($count == 0);
+ print("$text");
+ }
diff --git a/linhes/linhes-system/misc_which_recorder.pl b/linhes/linhes-system/misc_which_recorder.pl
new file mode 100755
index 0000000..57947f1
--- /dev/null
+++ b/linhes/linhes-system/misc_which_recorder.pl
@@ -0,0 +1,107 @@
+#!/usr/bin/perl -w
+#
+# Parses the backend log file and includes information on which encoder was used to record shows.
+
+my ($time, $title, $subtitle, $chanid, $cardid, $sourceid);
+my $index = 0;
+
+sub print_text
+{
+ print "$time - $title";
+ print ": $subtitle" if ($subtitle);
+ print "\n";
+ print " - Encoder ID: $cardid\n";
+ print " - Video Source ID: $sourceid\n";
+ print " - Channel ID: $chanid\n";
+}
+
+sub print_xml
+{
+ print "<a href=\"javascript:void(0)\">$time - $title";
+ print ": $subtitle" if ($subtitle);
+ print " - Encoder: $cardid<br />".
+ "<span><strong>$title</strong> $time<br />";
+ print "<em>$subtitle</em><br />" if ($subtitle);
+ print "<br />Channel ID: $chanid<br />Encoder ID: $cardid<br />".
+ "Video Source ID: $sourceid<br /></span></a><hr />";
+ # For XML parsers
+ print "[]:[]capture_info$index\[]:[]time='$time':title='$title'".
+ ":subtitle='$subtitle':chanid='$chanid':cardid='$cardid'".
+ ":sourceid='$sourceid'\n";
+}
+
+my $mode = shift;
+my $log_file = shift;
+
+if (($mode ne "--text") && ($mode ne "--noheader"))
+{
+ $log_file = $mode;
+}
+
+if ($log_file =~ m/^--.*/i)
+{
+ die ("Only one option can be used at a time.\n");
+}
+
+if ($log_file =~ /\.gz$/)
+{
+# read top down
+# open($fh, "gunzip -c $log_file |") or die "Unable to open log file '$log_file', stopping:";
+# read bottom up
+ open($fh, "gunzip -c $log_file |tac |") or die "Unable to open log file '$log_file', stopping:";
+}
+else
+{
+# read top down
+# open($fh, "<$log_file") or die "Unable to open log file '$log_file', stopping:";
+# read bottom up
+ open($fh, "tac $log_file |") or die "Unable to open log file '$log_file', stopping:";
+}
+
+while (<$fh>)
+{
+# Myth .25 & .27 regex to find start of recording for digital and analog MPEG
+ if (/^(\D\D\D +\d+ \d+\:\d+\:\d+)(?:.*) \(HandleRecordingStatusChange\) (?:Started|Tuning) recording: (.*): channel (\d+) on cardid \[(\d+)\], sourceid (\d+)/ || /^(\d+-\d+-\d+T\d+\:\d+\:\d+)(?:.*) \(UpdateRecStatus\) Updating status for (.*)() on cardid \[(\d+)\] \(Will Record => Recording\)()/)
+ {
+ if ($mode eq "--noheader")
+ {
+ print "<div class=\"schedule\">"
+ if (($index == 0) && ($mode ne "--text"));
+ }
+ else
+ {
+ print "<h3>Encoder Information</h3><div class=\"schedule\">"
+ if (($index == 0) && ($mode ne "--text"));
+ }
+ $index++;
+
+ ($time, $title, $chanid, $cardid, $sourceid) = ($1, $2, $3, $4, $5);
+ $time =~ s/T/' '/;
+ $time = `date -d "$time" +%a' '%-m/%-d' '%l:%M' '%p`;
+ chomp ($time);
+ if (($title =~ /"?(.+)"?:"?(.*)"?/) || ($title =~ /(.+) "(.*)"/))
+ {
+ $title = $1;
+ $subtitle = $2;
+ }
+ else
+ {
+ $subtitle = '';
+ }
+ $title =~ s/^"//;
+ $subtitle =~ s/^"//;
+ $title =~ s/"$//;
+ $subtitle =~ s/"$//;
+ if ($mode eq "--text")
+ {
+ print_text;
+ }
+ else
+ {
+ print_xml;
+ }
+ }
+}
+print "</div>" if (($index > 0) && ($mode ne "--text"));
+
+close $fh;
diff --git a/linhes/linhes-system/myth2mkv b/linhes/linhes-system/myth2mkv
new file mode 100755
index 0000000..6edb804
--- /dev/null
+++ b/linhes/linhes-system/myth2mkv
@@ -0,0 +1,466 @@
+#!/bin/bash
+#
+# Convert video to AVC-1 / h264
+#
+# version 0.27-001
+#
+# Prerequisites:
+# - mythtv >= 0.27
+# - handbrake-cli
+# - mplayer
+# - mkvtoolnix
+# - jobqueue_helper.py
+#
+# Arguments
+# $1 must be the directory/file of the recording
+# $2 must be chanid
+# $3 must be starttime
+# $4 must be title
+# $5 must be subtitle
+# $6 must be jobid
+# $7 must be quality of encode
+#
+# As a MythTV user job:
+# myth2mkv "%DIR%/%FILE%" "%CHANID%" "%STARTTIME%" "%TITLE%" "%SUBTITLE%" "%JOBID%" "HP|HQ|MQ|LQ"
+# Select only 1 quality setting
+# HP is similar to the HandBrake built-in preset High Profile
+
+########################
+# #
+# Adjustable variables #
+# #
+########################
+
+OUTDIR=/data/storage/disk0/media/video
+LOGPATH=/var/log/mythtv
+LOGFILE=${LOGPATH}/myth2mkv-$$.log
+
+# TMPDIR is for large transient files
+TMPDIR=/data/storage/disk0/media/tmp
+
+# x264 tuning:
+# Tune x264 based on content. Valid options for TUNING are:
+# film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency
+# Separate multiple options with a comma. DEFAULT: none
+TUNING=""
+
+# Custom cropping. Useful if you have a 4:3 image in a HD frame or if
+# HandBrake's autocrop smarts fail you.
+# Crop 240 pixels off the left and right for 4:3 image in 1920x1080 frame
+# Crop 160 pixels off the left and right for 4:3 image in 1280x720 frame
+# <T:B:L:R>
+# i.e. 0:0:240:240
+# Default: In HP and HQ: CROP="0:0:0:0" (no cropping).
+# IN MQ and LQ: autocrop.
+CROP=""
+
+# Force custom output resolution.
+# Default: Keep same resolution as input file (less any cropping).
+# The HP quality setting always keeps the same resolution as the input file.
+WIDTH=""
+HEIGHT=""
+
+# Force use/non-use of deinterlacing filter. Y|N|G (Yes, No, Guess)
+# Default: G - Guess based on source resolution.
+# If the source video width is 1920, 1440, 852, 704, 640 or 528 pixels
+# "G" will deinterlace the video. Change to "Y" to force use of deinterlacing
+# filter or to "N" to NOT use deinterlace filter no matter the resolution.
+DEINT="G"
+
+############################
+# #
+# End adjustable variables #
+# #
+############################
+
+if [[ ! -d ${TMPDIR} ]] ; then
+ mkdir -p ${TMPDIR}
+fi
+
+if [[ ! -d ${OUTDIR} ]] ; then
+ mkdir -p ${OUTDIR}
+fi
+
+#------FUNCTIONS---------------
+update_comment()
+# Arg_1 = COMMENT
+{
+if [ ${NO_JOBID} -eq 0 ]; then
+ `jobqueue_helper.py -j ${JOBID} -cs "${1}"`
+fi
+}
+
+check_background_progress()
+# check handbrake progress in background
+{
+while [ ! -f ${HB_RETURN_CODE} ]
+do
+ sleep 15
+ check_myth_jobcmds
+ pass=`tail -1 ${STATUSFILE} | egrep -o -e 'task [0-9] of [0-9], ' | tail -1 | sed 's/task\ /Pass\ /g'`
+ prog_percent=`tail -1 ${STATUSFILE} | egrep -o -e '[0-9]*\.[0-9]. %' | tail -1 | sed 's/\ %//g'`
+ current_FPS=`tail -1 ${STATUSFILE} | egrep -o -e 'avg [0-9.]*\.[0-9]* fps' | tail -1 | sed -e 's/avg\ //g' -e 's/\ fps//g'`
+ current_ETA=`tail -1 ${STATUSFILE} | egrep -o -e 'ETA [0-9.][0-9.]h[0-9.][0-9.]m[0-9.][0-9.]s' | tail -1`
+ if [ -n "$prog_percent" ]; then
+ echo "${pass}${prog_percent}% @ ${current_FPS} fps. ${current_ETA}"
+ update_comment "${pass}${prog_percent}% @ ${current_FPS} fps. ${current_ETA}"
+ fi
+done
+}
+
+check_myth_jobcmds()
+# check the myth database for stop pause or resume commands
+{
+if [[ ${NO_JOBID} -eq 0 ]] ; then
+ CURRENT_CMD=`jobqueue_helper.py -m "select cmds from jobqueue where id = ${JOBID}"`
+ case "${CURRENT_CMD}" in
+ # JOB_RUN
+ 0) ;;
+ # JOB_PAUSE
+ 1) `jobqueue_helper.py -j ${JOBID} -ss 6`
+ kill -s STOP ${handbrake_pid} ;;
+ # JOB_RESUME
+ 2) `jobqueue_helper.py -j ${JOBID} -ss 4`
+ `jobqueue_helper.py -j ${JOBID} -cmds 0`
+ kill -s CONT ${handbrake_pid} ;;
+ # JOB_STOP
+ 4) `jobqueue_helper.py -j ${JOBID} -ss 5`
+ `jobqueue_helper.py -j ${JOBID} -cmds 0`
+ kill -9 ${handbrake_pid} ${command_pid}
+ clean_up_files
+ echo "Encode Cancelled" >> ${LOGFILE}
+ `jobqueue_helper.py -j ${JOBID} -ss 320`
+ exit ;;
+ esac
+fi
+}
+
+get_info_for_hb() {
+# Collect some info about source file
+
+/usr/bin/mplayer -nolirc -identify -frames 0 "${HBINPUTFILE}" \
+ 2>/dev/null 1>"${IDFILE}"
+
+VIDEOW=$( grep ID_VIDEO_WIDTH= "${IDFILE}" | awk -F= '{ print $NF }' )
+FPS=$( grep ID_VIDEO_FPS= "${IDFILE}" | awk -F= '{ print $NF }' )
+
+# HandBrake does not like a framerate of 29.970, so let's drop the 0
+if [[ ${FPS} = "29.970" ]] ; then
+ FPS="29.97"
+fi
+
+# HandBrake does not like a framerate of 59.940, so let's drop the 0
+if [[ ${FPS} = "59.940" ]] ; then
+ FPS="59.94"
+fi
+
+# A rough guestimation that if the video width is 1920, 1440, 852, 704, 640 or
+# 528 pixels it is probably interlaced.
+if [[ ${DEINT} = Y ]] ; then
+ DEINT="-d slow"
+else
+ if [[ ${DEINT} = N ]] ; then
+ DEINT=""
+ else
+ if [[ ${VIDEOW} = 1920 || ${VIDEOW} = 1440 || ${VIDEOW} = 852 || \
+ ${VIDEOW} = 704 || ${VIDEOW} = 640 || ${VIDEOW} = 528 ]] ; then
+ DEINT="-d slow"
+ else
+ DEINT=""
+ fi
+ fi
+fi
+
+if [[ -n ${DEINT} ]] ; then
+ if [[ ${QUALITY} = LQ ]] ; then
+ DEINT="-d fast"
+ fi
+fi
+
+if [[ -n ${TUNING} ]] ; then
+ TUNING="--x264-tune ${TUNING}"
+fi
+
+if [[ -n ${CROP} ]] ; then
+ CROP="--crop ${CROP}"
+fi
+
+if [[ -n ${WIDTH} ]] ; then
+ WIDTH="-w ${WIDTH} -X ${WIDTH}"
+fi
+
+if [[ -n ${HEIGHT} ]] ; then
+ HEIGHT="-l ${HEIGHT} -Y ${HEIGHT}"
+fi
+
+if [[ ${QUALITY} = HP ]] ; then
+ if [[ -n ${CROP} ]] ; then
+ CROP="--crop ${CROP}"
+ else
+ CROP="--crop 0:0:0:0 --auto-anamorphic"
+ fi
+ HB_OPTS="-o ${TMPFILE} -e x264 ${TUNING} -q 20.0 -a 1,1 -E copy:ac3,faac -B 160,160 -6 dpl2,auto -R Auto,Auto -D 0.0,0.0 --audio-copy-mask aac,ac3,dtshd,dts,mp3 --audio-fallback ffac3 -f mkv --decomb --loose-anamorphic --modulus 2 -m --x264-preset medium --h264-profile high --h264-level 4.1 ${CROP} -s 1"
+elif [[ ${QUALITY} = HQ ]] ; then
+ if [[ -n ${CROP} ]] ; then
+ CROP="--crop ${CROP}"
+ else
+ CROP="--crop 0:0:0:0 --auto-anamorphic"
+ fi
+ HB_OPTS="-o ${TMPFILE} -f mkv -m -e x264 ${TUNING} -x b-adapt=2:rc-lookahead=50 -b 5000 -2 -T ${WIDTH} ${HEIGHT} -r ${FPS} --cfr ${CROP} ${DEINT} -a 1 -E copy -s 1"
+else
+ if [[ ${CROP} = "--crop 0:0:0:0" ]] ; then
+ CROP="${CROP} --auto-anamorphic"
+ fi
+ if [[ ${QUALITY} = LQ ]] ; then
+ HB_OPTS="-o ${TMPFILE} -f mkv -m -e x264 ${TUNING} -b 1250 ${WIDTH} ${HEIGHT} -r ${FPS} --pfr ${CROP} ${DEINT} -a 1 -E lame -B 128 -Q 8 -6 stereo -s 1"
+ else
+ # Fallback to "MQ"
+ HB_OPTS="-o ${TMPFILE} -f mkv -m -e x264 ${TUNING} -b 2500 -2 -T ${WIDTH} ${HEIGHT} -r ${FPS} --pfr ${CROP} ${DEINT} -a 1 -E lame -B 256 -Q 3 -6 stereo -s 1"
+ fi
+fi
+}
+
+get_handbrake_pid()
+{
+process_name=""
+i1=1
+while [ "${process_name}" != "found" ]; do
+ handbrake_pid=`expr ${handbrake_pid} + 1`
+ i1=`expr ${i1} + 1`
+ if [ "`ps ${handbrake_pid} | grep HandBrakeCLI | sed 's_.*\(HandBrakeCLI\).*_\1_'`" = "HandBrakeCLI" ]; then
+ process_name="found"
+ fi
+ if [ ${i1} -gt 20 ]; then
+ break
+ fi
+done
+}
+
+tag_file() {
+DATE=`date`
+
+# Create a tag file here
+echo "<?xml version='1.0' encoding='ISO-8859-1'?>" > "${TAG_FILE}"
+echo "" >> "${TAG_FILE}"
+echo "<!DOCTYPE Tags SYSTEM 'matroskatags.dtd'>" >> "${TAG_FILE}"
+echo "" >> "${TAG_FILE}"
+echo "<Tags>" >> "${TAG_FILE}"
+echo " <Tag>" >> "${TAG_FILE}"
+echo " <Simple>" >> "${TAG_FILE}"
+echo " <Name>TITLE</Name>" >> "${TAG_FILE}"
+echo " <String>${TITLE}</String>" >> "${TAG_FILE}"
+echo " <Simple>" >> "${TAG_FILE}"
+echo " <Name>SUBTITLE</Name>" >> "${TAG_FILE}"
+echo " <String>${SUBTITLE}</String>" >> "${TAG_FILE}"
+echo " <Simple>" >> "${TAG_FILE}"
+echo " <Name>SUMMARY</Name>" >> "${TAG_FILE}"
+echo " <String>${DESCR}</String>" >> "${TAG_FILE}"
+echo " </Simple>" >> "${TAG_FILE}"
+echo " <Simple>" >> "${TAG_FILE}"
+echo " <Name>DATE_RELEASED</Name>" >> "${TAG_FILE}"
+echo " <String>${OAD}</String>" >> "${TAG_FILE}"
+echo " </Simple>" >> "${TAG_FILE}"
+echo " <Simple>" >> "${TAG_FILE}"
+echo " <Name>SEASON</Name>" >> "${TAG_FILE}"
+echo " <String>${SEASON}</String>" >> "${TAG_FILE}"
+echo " </Simple>" >> "${TAG_FILE}"
+echo " <Simple>" >> "${TAG_FILE}"
+echo " <Name>EPISODE</Name>" >> "${TAG_FILE}"
+echo " <String>${EPISODE}</String>" >> "${TAG_FILE}"
+echo " </Simple>" >> "${TAG_FILE}"
+echo " </Simple>" >> "${TAG_FILE}"
+echo " </Simple>" >> "${TAG_FILE}"
+echo " <Simple>" >> "${TAG_FILE}"
+echo " <Name>ENCODED_BY</Name>" >> "${TAG_FILE}"
+echo " <String>HandBrakeCLI ${HBCLIVER}</String>" >> "${TAG_FILE}"
+echo " </Simple>" >> "${TAG_FILE}"
+echo " <Simple>" >> "${TAG_FILE}"
+echo " <Name>DATE_TAGGED</Name>" >> "${TAG_FILE}"
+echo " <String>${DATE}</String>" >> "${TAG_FILE}"
+echo " </Simple>" >> "${TAG_FILE}"
+echo " </Tag>" >> "${TAG_FILE}"
+echo "</Tags>" >> "${TAG_FILE}"
+
+# Add tag info into MKV file
+echo "Adding tag info to ${TITLE} - ${SUBTITLE} ..." >> ${LOGFILE}
+
+/usr/bin/mkvpropedit -r ${LOGFILE} -t all:"${TAG_FILE}" "${TMPFILE}"
+}
+
+clean_up_files()
+# clean up left over files
+{
+unlink ${TMPFILE} 2> /dev/null
+unlink ${TMPCUTFILE} 2> /dev/null
+unlink ${TMPCUTFILE}.map 2> /dev/null
+unlink ${STATUSFILE} 2> /dev/null
+unlink ${IDFILE} 2> /dev/null
+unlink ${HB_RETURN_CODE} 2> /dev/null
+unlink ${TAG_FILE} 2> /dev/null
+}
+
+#-------MAIN SCRIPT------------
+
+# create temp filename so multiple instances won't conflict
+TMPNAME=toX264-$$
+TMPFILE=${TMPDIR}/${TMPNAME}.mkv
+TMPCUTFILE=${TMPDIR}/${TMPNAME}.mpg
+HBINPUTFILE="${1}"
+TITLE="${4}"
+SUBTITLE="${5}"
+JOBID="${6}"
+QUALITY="${7}"
+BASE=`basename ${HBINPUTFILE}`
+HBCLIVER=`pacman -Q | grep handbrake-cli | awk '{ print $NF }' | awk -F"-" '{ print $1 }'`
+STATUSFILE=/tmp/${TMPNAME}-status.log
+HB_RETURN_CODE=/tmp/${TMPNAME}-hb_return_code
+IDFILE=/tmp/${TMPNAME}-id.txt
+TAG_FILE=/tmp/${TMPNAME}.xml
+SEASON=`jobqueue_helper.py -m "select season from recorded where basename LIKE '${BASE}'"`
+SEASON=`printf "%02d" $SEASON`
+EPISODE=`jobqueue_helper.py -m "select episode from recorded where basename LIKE '${BASE}'"`
+EPISODE=`printf "%02d" $EPISODE`
+OAD=`jobqueue_helper.py -m "select originalairdate from recorded where basename LIKE '${BASE}'"`
+DESCR=`jobqueue_helper.py -m "select description from recorded where basename LIKE '${BASE}'" | sed 's/\&/and/g'`
+USER=`whoami`
+
+# check if %JOBID% is passed from command line
+if [ -z ${JOBID} ]; then
+ NO_JOBID=1
+else
+ NO_JOBID=0
+fi
+
+# log file location
+CDate="`date`"
+echo "" >> ${LOGFILE}
+echo $CDate >> ${LOGFILE}
+echo "File to encode: ${HBINPUTFILE}" >> ${LOGFILE}
+echo " --> Name: ${TITLE} - ${SUBTITLE}" >> ${LOGFILE}
+echo " --> Temporary Files: ${TMPNAME}.*" >> ${LOGFILE}
+echo "" >> ${LOGFILE}
+
+get_info_for_hb
+ERROR=$?
+
+if [[ ${ERROR} != 0 ]] ; then
+ echo "Error parsing source file information!" >> ${LOGFILE}
+ cat ${IDFILE} >> ${LOGFILE}
+ clean_up_files
+ exit 1
+fi
+
+# start timer
+beforetime="$(date +%s)"
+
+check_myth_jobcmds
+
+# If there is a cutlist, use it:
+if [[ -n `mythutil --getcutlist --chanid "${2}" --starttime "${3}" | grep \
+ Cutlist: | awk -F": " '{ print $NF }'` ]] ; then
+ echo "Applying cutlist for ${TITLE} - ${SUBTITLE} ..." >> ${LOGFILE}
+ mythtranscode --chanid "${2}" --starttime "${3}" -m --honorcutlist \
+ -q --loglevel info --logpath "${LOGPATH}" -o "${TMPCUTFILE}"
+ mythtrans_pid=$!
+ ERROR=$?
+ HBINPUTFILE=${TMPCUTFILE}
+fi
+
+if [[ ${ERROR} != 0 ]] ; then
+ echo "MythTranscode error!" >> ${LOGFILE}
+ echo "Check ${LOGPATH}/mythtranscode.date.${mythtrans_pid}.log for mythtranscode error" >> ${LOGFILE}
+ clean_up_files
+ exit 1
+fi
+
+# run handbrake in background to do conversion
+echo "Encoding ${TITLE} - ${SUBTITLE} ..." >> ${LOGFILE}
+
+( /usr/bin/nice -n19 nohup /usr/bin/HandBrakeCLI -i ${HBINPUTFILE} ${HB_OPTS} \
+ > ${STATUSFILE} 2>&1 ; echo $? > ${HB_RETURN_CODE} ) &
+handbrake_pid=$!
+command_pid=${handbrake_pid}
+get_handbrake_pid
+
+check_background_progress
+
+if [[ `cat ${HB_RETURN_CODE}` != 0 ]] ; then
+ echo "HandBrakeCLI error!" >> ${LOGFILE}
+ cat ${STATUSFILE} >> ${LOGFILE}
+ clean_up_files
+ exit 1
+fi
+
+tag_file
+ERROR=$?
+
+if [[ ${ERROR} != 0 ]] ; then
+ echo "Error creating tag file!" >> ${LOGFILE}
+ cat ${TAG_FILE} >> ${LOGFILE}
+ clean_up_files
+ exit 1
+fi
+
+# make output filename unique and do not clobber an existing file
+# Build a final file name
+if [[ $SEASON != "00" && $EPISODE != "00" ]]; then
+ FILE=$( echo "${TITLE,,} s${SEASON}e${EPISODE} ${SUBTITLE,,}" | tr -d [:punct:] | tr [:blank:] "_" | tr -s "_" )
+else
+ FILE=$( echo "${TITLE,,} ${OAD} ${SUBTITLE,,}" | tr -d [:punct:] | tr [:blank:] "_" | tr -s "_" )
+fi
+OUTPUTFILE="${OUTDIR}/${FILE}.mkv"
+i=1
+while [ -e "${OUTPUTFILE}" ]
+do
+ OUTPUTFILE="${OUTDIR}/${FILE}-${i}.mkv"
+ i=`expr $i + 1`
+done
+
+# move temp file to output location
+chown -v "${USER}" "${TMPFILE}" >> ${LOGFILE}
+mv -v "${TMPFILE}" "$OUTPUTFILE" >> ${LOGFILE}
+ERROR=$?
+
+if [[ ${ERROR} != 0 ]] ; then
+ echo "Error moving ${TMPFILE} to ${OUTPUTFILE} !" >> ${LOGFILE}
+ clean_up_files
+ exit 1
+fi
+
+# stop timer
+aftertime="$(date +%s)"
+seconds="$(expr ${aftertime} - ${beforetime})"
+
+if [ ${ERROR} -eq 0 ]; then
+ echo "File Encoded Successfully: ${OUTPUTFILE}" >> ${LOGFILE}
+ hours=$((seconds / 3600))
+ seconds=$((seconds % 3600))
+ minutes=$((seconds / 60))
+ seconds=$((seconds % 60))
+ if [ $hours -eq 0 ]; then
+ hours=""
+ elif [ $hours -eq 1 ]; then
+ hours=" $hours hour"
+ else
+ hours=" $hours hours"
+ fi
+ if [ $minutes -eq 1 ]; then
+ minutes="$minutes minute"
+ else
+ minutes="$minutes minutes"
+ fi
+ if [ $seconds -eq 1 ]; then
+ seconds="$seconds second"
+ else
+ seconds="$seconds seconds"
+ fi
+
+ echo "Encoding took${hours} ${minutes} ${seconds} @ ${current_FPS} fps." >> ${LOGFILE}
+ `jobqueue_helper.py -j ${JOBID} -ss 272`
+ update_comment "Encode Successful. Encoding Time:${hours} ${minutes} ${seconds}"
+else
+ echo "ERROR: ${ERROR}" >> ${LOGFILE}
+fi
+
+# Clean up
+clean_up_files
diff --git a/linhes/linhes-system/myth2mp3 b/linhes/linhes-system/myth2mp3
new file mode 100755
index 0000000..e5960be
--- /dev/null
+++ b/linhes/linhes-system/myth2mp3
@@ -0,0 +1,96 @@
+#!/bin/sh
+# convert recordings to as mp3 audio only
+# version 1.1.3
+
+# usage:
+# first parameter must be %DIR%/%FILE% of the recording
+# second parameter must be the desired base name of the output
+# third parameter must be %CHANID% if you set USECUTLIST=Y
+# fourth parameter must be %STARTTIME% if you set USECUTLIST=Y
+# In the mythtv setup screen invoke this script like this:
+# MYTHTV User Job Command:
+# /usr/bin/myth2mp3 "%DIR%/%FILE%" "%TITLE% - %SUBTITLE%" "%CHANID%" "%STARTTIME%"
+
+# options:
+BITRATE=256k #ie. 128k, 160k, 192k, 224k, 256k
+USECUTLIST=Y #Y or N
+
+# where the converted audio is stored
+OUT_DIR=/data/storage/disk0/media/music
+
+# create temp filename so multiple instances won't conflict
+TMPNAME=toMP3-$$
+TMPFILE=/data/storage/disk0/media/tmp/$TMPNAME
+TMPCUTFILE=/data/storage/disk0/media/tmp/$TMPNAME.mpg
+FFINPUTFILE=$1
+TITLE=`echo $2 | sed 's/\//_/g'`
+
+# log file location
+LOGFILE=/var/log/mythtv/myth2mp3.log
+CDate="`date`"
+echo "" >> $LOGFILE
+echo $CDate >> $LOGFILE
+echo "File to encode: $1 Name: $TITLE" >> $LOGFILE
+
+# start timer
+beforetime="$(date +%s)"
+
+# check if using cutlist
+if [ $USECUTLIST = Y ];then
+ MYTHCOMMFRAMES=`mythutil --getcutlist --chanid "$3" --starttime "$4" | grep 'Cutlist:' | cut -d \ -f 2`
+ if [ -n "$MYTHCOMMFRAMES" ]; then
+ echo "Extracting Cutlist..." >> $LOGFILE
+ /usr/bin/nice -n19 /usr/bin/mythtranscode --chanid "$3" --starttime "$4" --outfile "$TMPCUTFILE" --mpeg2 --honorcutlist
+ FFINPUTFILE=$TMPCUTFILE
+ fi
+fi
+
+# run ffmpeg to do conversion to wav
+echo "Encoding to intermediate wav..." >> $LOGFILE
+/usr/bin/nice -n19 /usr/bin/ffmpeg -i "$FFINPUTFILE" -vn -acodec pcm_s16le -ar 44100 -ac 2 "$TMPFILE.wav"
+ERROR=$?
+
+# Normalize the intermediate wav
+echo "Normalizing intermediate wav..." >> $LOGFILE
+/usr/bin/nice -n19 /usr/bin/normalize -q "$TMPFILE.wav"
+ERROR=$?
+
+FFINPUTFILE=$TMPFILE.wav
+
+# Final encode of normalized wav to mp3
+echo "Encoding normalized wav to mp3..." >> $LOGFILE
+/usr/bin/nice -n19 /usr/bin/ffmpeg -i "$FFINPUTFILE" -vn -acodec libmp3lame -ab $BITRATE -ar 44100 -ac 2 "$TMPFILE.mp3"
+ERROR=$?
+
+# make output filename unique
+OUTPUTFILE=$OUT_DIR/$TITLE.mp3
+i=1
+while [ -e "$OUTPUTFILE" ]
+do
+ OUTPUTFILE=$OUT_DIR/$TITLE-$i.mp3
+ i=`expr $i + 1`
+done
+
+# move temp file to output location
+chown mythtv "$TMPFILE.mp3" && mv "$TMPFILE.mp3" "$OUTPUTFILE"
+
+# stop timer
+aftertime="$(date +%s)"
+seconds="$(expr $aftertime - $beforetime)"
+
+if [ $ERROR -eq 0 ]; then
+ echo "File Encoded Sucessfully: $OUTPUTFILE" >> $LOGFILE
+ hours=$((seconds / 3600))
+ seconds=$((seconds % 3600))
+ minutes=$((seconds / 60))
+ seconds=$((seconds % 60))
+ echo "Encoding Time: $hours hour(s) $minutes minute(s) $seconds second(s)" >> $LOGFILE
+else
+ echo "ERROR: $ERROR" >> $LOGFILE
+fi
+
+# clean up left over files
+unlink $TMPFILE.mp3 2> /dev/null
+unlink $TMPFILE.wav 2> /dev/null
+unlink $TMPCUTFILE 2> /dev/null
+unlink $TMPCUTFILE.map 2> /dev/null
diff --git a/linhes/linhes-system/myth2videos b/linhes/linhes-system/myth2videos
new file mode 100755
index 0000000..c4d3246
--- /dev/null
+++ b/linhes/linhes-system/myth2videos
@@ -0,0 +1,148 @@
+#!/bin/sh
+# copy recording to videos
+# version 0.3
+
+# usage:
+# first parameter must be %DIR%/%FILE% of the recording
+# second parameter must be the desired base name of the output
+# third parameter must be %CHANID% if you set USECUTLIST=Y
+# fourth parameter must be %STARTTIME% if you set USECUTLIST=Y
+# fifth parameter must be %JOBID% for the User Job status to be updated in MythTV
+# in the mythtv setup screen invoke this script like this:
+# MYTHTV User Job Command:
+# /usr/bin/myth2videos "%DIR%/%FILE%" "%TITLE% - %SUBTITLE%" "%CHANID%" "%STARTTIME%" "%JOBID%"
+
+# options:
+USECUTLIST=Y # Y or N
+
+# where the video is stored
+OUT_DIR=/data/storage/disk0/media/video
+
+
+#------FUNCTIONS---------------
+update_comment()
+# Arg_1 = COMMENT
+{
+if [ $NO_JOBID -eq 0 ]; then
+ `jobqueue_helper.py -j ${JOBID} -cs "${1}"`
+fi
+}
+
+update_status()
+# Arg_1 = status code
+{
+if [ $NO_JOBID -eq 0 ]; then
+ `jobqueue_helper.py -j ${JOBID} -cs "${1}"`
+fi
+}
+
+check_myth_jobcmds()
+# check the myth database for stop pause or resume commands
+{
+if [ $NO_JOBID -eq 0 ]; then
+ CURRENT_CMD=`jobqueue_helper.py -m "select cmds from jobqueue where id = ${JOBID}"`
+ case "$CURRENT_CMD" in
+ # JOB_RUN
+ 0) ;;
+ # JOB_PAUSE
+ 1) `jobqueue_helper.py -j ${JOBID} -ss 6`;;
+ # JOB_RESUME
+ 2) `jobqueue_helper.py -j ${JOBID} -ss 4`
+ `jobqueue_helper.py -j ${JOBID} -cmds 0`;;
+ # JOB_STOP
+ 4) `jobqueue_helper.py -j ${JOBID} -ss 5`
+ `jobqueue_helper.py -j ${JOBID} -cmds 0`
+ clean_up_files
+ echo "Copy Cancelled" >> $LOGFILE
+ `jobqueue_helper.py -j ${JOBID} -ss 320`
+ exit ;;
+ esac
+fi
+}
+
+clean_up_files()
+# clean up left over files
+{
+unlink $TMPFILE 2> /dev/null
+unlink $TMPFILE.map 2> /dev/null
+}
+
+#-------MAIN SCRIPT------------
+
+# check if %JOBID% is passed from command line
+JOBID=${5}
+if [ -z "$JOBID" ]; then
+ NO_JOBID=1
+else
+ NO_JOBID=0
+fi
+
+# create temp filename so multiple instances won't conflict
+TMPNAME=toVIDEOS-$$
+TMPFILE=/data/storage/disk0/media/tmp/$TMPNAME.mpg
+MENINPUTFILE=$1
+TITLE=`echo $2 | sed 's/\//_/g'`
+
+# log file location
+LOGFILE=/var/log/mythtv/myth2videos.log
+CDate="`date`"
+echo "" >> $LOGFILE
+echo $CDate >> $LOGFILE
+echo "File to copy: $MENINPUTFILE Name: $TITLE" >> $LOGFILE
+echo "$2 $3 $4 $5" >> $LOGFILE
+
+# start timer
+beforetime="$(date +%s)"
+
+check_myth_jobcmds
+
+# check if using cutlist
+if [ $USECUTLIST = Y ]; then
+ MYTHCOMMFRAMES=`mythutil --getcutlist --chanid "$3" --starttime "$4" | grep 'Cutlist:' | cut -d \ -f 2`
+ echo $MYTHCOMMFRAMES
+ if [ -n "$MYTHCOMMFRAMES" ]; then
+ echo "Extracting Cutlist..." >> $LOGFILE
+ update_comment "Extracting Cutlist..."
+ /usr/bin/nice -n19 /usr/bin/mythtranscode --chanid "$3" --starttime "$4" --outfile "$TMPFILE" --mpeg2 --honorcutlist
+ else
+ update_comment "Copying Recording..."
+ cp "$MENINPUTFILE" "$TMPFILE"
+ fi
+elif [ $USECUTLIST = N ]; then
+ update_comment "Copying Recording..."
+ cp "$MENINPUTFILE" "$TMPFILE"
+fi
+
+# make output filename unique
+OUTPUTFILE=$OUT_DIR/$TITLE.mpg
+i=1
+while [ -e "$OUTPUTFILE" ]
+do
+ OUTPUTFILE=$OUT_DIR/$TITLE-$i.mpg
+ i=`expr $i + 1`
+done
+
+# move temp file to output location
+chown mythtv "$TMPFILE" && mv "$TMPFILE" "$OUTPUTFILE"
+
+# stop timer
+aftertime="$(date +%s)"
+seconds="$(expr $aftertime - $beforetime)"
+
+ERROR=$?
+if [ $ERROR -eq 0 ]; then
+ echo "File Encoded Successfully: $OUTPUTFILE" >> $LOGFILE
+ hours=$((seconds / 3600))
+ seconds=$((seconds % 3600))
+ minutes=$((seconds / 60))
+ seconds=$((seconds % 60))
+ echo "Encoding took $hours hour(s) $minutes minute(s) $seconds second(s) @ $current_FPS fps." >> $LOGFILE
+ update_status 272
+ update_comment "Encode Successful. Encoding Time: $hours hour(s) $minutes minute(s) $seconds second(s)"
+else
+ update_status 304
+ update_comment "Encode Failed. Exit status: $ERROR"
+ echo "ERROR: $ERROR" >> $LOGFILE
+fi
+
+clean_up_files
diff --git a/linhes/linhes-system/openssh.hook b/linhes/linhes-system/openssh.hook
new file mode 100644
index 0000000..0f9f866
--- /dev/null
+++ b/linhes/linhes-system/openssh.hook
@@ -0,0 +1,9 @@
+[Trigger]
+Operation = Install
+Type = Package
+Target = linhes-system
+
+[Action]
+Description = Enable and start sshd...
+When = PostTransaction
+Exec = /usr/bin/systemctl enable --now sshd.service
diff --git a/linhes/linhes-system/optimize_mythdb.py b/linhes/linhes-system/optimize_mythdb.py
new file mode 100755
index 0000000..7d60e89
--- /dev/null
+++ b/linhes/linhes-system/optimize_mythdb.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+# import MySQL module
+import MySQLdb
+import socket
+import sys
+db = MySQLdb.connect(host="localhost", user="mythtv", passwd="mythtv", db="mythconverg")
+cursor = db.cursor()
+cursor.execute("SHOW tables")
+result = cursor.fetchall()
+ops=["REPAIR","OPTIMIZE","ANALYZE"]
+for row in result:
+ ctable=row[0]
+ for op in ops:
+ print(op,ctable)
+ cmd= "%s table %s" %(op,ctable)
+ cursor.execute(cmd)
diff --git a/linhes/linhes-system/paccache.cron b/linhes/linhes-system/paccache.cron
new file mode 100644
index 0000000..d3ca082
--- /dev/null
+++ b/linhes/linhes-system/paccache.cron
@@ -0,0 +1,2 @@
+#!/bin/bash
+/usr/bin/paccache -r -q
diff --git a/linhes/linhes-system/plex_lib.conf b/linhes/linhes-system/plex_lib.conf
new file mode 100644
index 0000000..df4b3f3
--- /dev/null
+++ b/linhes/linhes-system/plex_lib.conf
@@ -0,0 +1,9 @@
+<!--#PLEX-->
+ <button>
+ <type>MENU_PLEX</type>
+ <text>Launch Plex</text>
+ <description>Open Plex Home Theater</description>
+ <action>EXEC /var/lib/flatpak/exports/bin/tv.plex.PlexHTPC</action>
+ </button>
+<!--#PLEX-->
+
diff --git a/linhes/linhes-system/plexmediascanner.sh b/linhes/linhes-system/plexmediascanner.sh
new file mode 100755
index 0000000..6ce2f5d
--- /dev/null
+++ b/linhes/linhes-system/plexmediascanner.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+exec 2>&1
+export TERM=linux
+. /etc/conf.d/plexmediaserver
+
+sudo -uplex -gplex LD_LIBRARY_PATH="${LD_LIBRARY_PATH}" PLEX_MEDIA_SERVER_HOME="${PLEX_MEDIA_SERVER_HOME}" PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR="${PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR}" /usr/lib/plexmediaserver/Plex\ Media\ Scanner $@
diff --git a/linhes/linhes-system/rc6_mce.toml b/linhes/linhes-system/rc6_mce.toml
new file mode 100644
index 0000000..9ba3cb8
--- /dev/null
+++ b/linhes/linhes-system/rc6_mce.toml
@@ -0,0 +1,70 @@
+# Generated with gen_keytables.pl from drivers/media/rc/keymaps/rc-rc6-mce.c
+[[protocols]]
+name = "rc6_mce"
+protocol = "rc6"
+variant = "rc6_mce"
+[protocols.scancodes]
+0x800f0400 = "KEY_NUMERIC_0"
+0x800f0401 = "KEY_NUMERIC_1"
+0x800f0402 = "KEY_NUMERIC_2"
+0x800f0403 = "KEY_NUMERIC_3"
+0x800f0404 = "KEY_NUMERIC_4"
+0x800f0405 = "KEY_NUMERIC_5"
+0x800f0406 = "KEY_NUMERIC_6"
+0x800f0407 = "KEY_NUMERIC_7"
+0x800f0408 = "KEY_NUMERIC_8"
+0x800f0409 = "KEY_NUMERIC_9"
+0x800f040a = "KEY_DELETE"
+0x800f040b = "KEY_ENTER"
+0x800f040c = "KEY_SLEEP"
+0x800f040d = "KEY_M"
+0x800f040e = "KEY_MUTE"
+0x800f040f = "KEY_I"
+0x800f0410 = "KEY_VOLUMEUP"
+0x800f0411 = "KEY_VOLUMEDOWN"
+0x800f0412 = "KEY_CHANNELUP"
+0x800f0413 = "KEY_CHANNELDOWN"
+0x800f0414 = "KEY_FASTFORWARD"
+0x800f0415 = "KEY_REWIND"
+0x800f0416 = "KEY_SPACE"
+0x800f0417 = "KEY_RECORD"
+0x800f0418 = "KEY_SPACE"
+0x800f0419 = "KEY_X"
+0x800f041a = "KEY_END"
+0x800f041b = "KEY_HOME"
+0x800f041c = "KEY_NUMERIC_POUND"
+0x800f041d = "KEY_NUMERIC_STAR"
+0x800f041e = "KEY_UP"
+0x800f041f = "KEY_DOWN"
+0x800f0420 = "KEY_LEFT"
+0x800f0421 = "KEY_RIGHT"
+0x800f0422 = "KEY_ENTER"
+0x800f0423 = "KEY_BACK"
+0x800f0424 = "KEY_DVD"
+0x800f0425 = "KEY_TUNER"
+0x800f0426 = "KEY_EPG"
+0x800f0427 = "KEY_ZOOM"
+0x800f0432 = "KEY_MODE"
+0x800f0433 = "KEY_PRESENTATION"
+0x800f0434 = "KEY_EJECTCD"
+0x800f043a = "KEY_BRIGHTNESSUP"
+0x800f0446 = "KEY_TV"
+0x800f0447 = "KEY_AUDIO"
+0x800f0448 = "KEY_PVR"
+0x800f0449 = "KEY_CAMERA"
+0x800f044a = "KEY_VIDEO"
+0x800f044c = "KEY_LANGUAGE"
+0x800f044d = "KEY_TITLE"
+0x800f044e = "KEY_PRINT"
+0x800f0450 = "KEY_RADIO"
+0x800f045a = "KEY_SUBTITLE"
+0x800f045b = "KEY_RED"
+0x800f045c = "KEY_GREEN"
+0x800f045d = "KEY_YELLOW"
+0x800f045e = "KEY_BLUE"
+0x800f0465 = "KEY_POWER2"
+0x800f0469 = "KEY_MESSENGER"
+0x800f046e = "KEY_PLAYPAUSE"
+0x800f046f = "KEY_PLAYER"
+0x800f0480 = "KEY_BRIGHTNESSDOWN"
+0x800f0481 = "KEY_PLAYPAUSE"
diff --git a/linhes/linhes-system/readme_is_xml b/linhes/linhes-system/readme_is_xml
new file mode 100644
index 0000000..f0a13cb
--- /dev/null
+++ b/linhes/linhes-system/readme_is_xml
@@ -0,0 +1 @@
+Files ending in .conf will be read by gen_is_xml.py or gen_lib_xml.py or gen_game_xml.py and output xml as appropriate
diff --git a/linhes/linhes-system/recordings.cron b/linhes/linhes-system/recordings.cron
new file mode 100644
index 0000000..d71c139
--- /dev/null
+++ b/linhes/linhes-system/recordings.cron
@@ -0,0 +1,144 @@
+#!/bin/bash
+
+#check if mythbackend is running and was just started
+for i in 1 2
+do
+if [ $(pidof mythbackend) ]
+then
+ now=$(date +%s)
+ mythbackendStartTime=$(systemctl status mythbackend.service | grep Active| cut -d ' ' -f 11-12)
+ mythbackendStartTime=$(date -d"$mythbackendStartTime" +"%s")
+ if [[ $(( $now - $mythbackendStartTime )) -lt 59 ]]
+ then
+ #echo "mythbackend started less than a minute ago. Sleeping..."
+ sleep 60
+ fi
+else
+ #echo "mythbackend not running. exiting."
+ exit
+fi
+done
+
+if [ -f /usr/share/mythtv/contrib/user_jobs/mythlink.pl ]
+then
+ recdir="/data/storage/disk0/media/recordings"
+ tmprecdir="/data/storage/disk0/media/tmp/recordings"
+ rm -r $tmprecdir
+ su - mythtv -c "perl /usr/share/mythtv/contrib/user_jobs/mythlink.pl --format '%Ct/%U/%T/%T %- S%ssE%ep %- %oY-%om-%od = %S' --link '$tmprecdir'"
+
+ # rename category_types (%Ct) from numbers to names
+ for cattype in $tmprecdir/*
+ do
+ if [ $cattype == "$tmprecdir/1" ]
+ then
+ rsync -a "$cattype/" "$tmprecdir/Movies"
+ rm -r "$cattype"
+ elif [[ $cattype == "$tmprecdir/2" ]] || [[ $cattype == "$tmprecdir/4" ]]
+ then
+ rsync -a "$cattype/" "$tmprecdir/TV Shows"
+ rm -r "$cattype"
+ elif [ $cattype == "$tmprecdir/3" ]
+ then
+ rsync -a "$cattype/" "$tmprecdir/Sports"
+ rm -r "$cattype"
+ else
+ #ignore Movies, TV Shows, Sports. Move all others to TV Shows
+ if [[ $cattype != "$tmprecdir/Movies" ]] && [[ $cattype != "$tmprecdir/TV Shows" ]] && [[ $cattype != "$tmprecdir/Sports" ]]
+ then
+ if [ ! -d "$tmprecdir/TV Shows" ]
+ then
+ mkdir "$tmprecdir/TV Shows"
+ fi
+ rsync -a "$cattype" "$tmprecdir/TV Shows"
+ rm -r "$cattype"
+ fi
+ fi
+ done
+
+ #delete Deleted recgroup
+ for link in $tmprecdir/**/Deleted
+ do
+ rm -r "$link"
+ done
+
+ #move all links in recgroup dirs out to parent dir
+ for recgroup in $tmprecdir/**/*
+ do
+ if [ -d "$recgroup" ]
+ then
+ cd "$recgroup"
+ rsync -a "$recgroup/" ..
+ cd "$tmprecdir"
+ rm -r "$recgroup"
+ fi
+ done
+
+ #replace SE if no season/episode is in myth
+ for link in $tmprecdir/**/**/*\ -\ SE\ -\ *
+ do
+ newlink=`echo "$link" | sed 's/ - SE - / - /'`
+ mv "$link" "$newlink"
+ done
+ #replace SEyy if no season is in myth
+ for link in $tmprecdir/**/**/*\ -\ SE[0-9][0-9]\ -\ *
+ do
+ newlink=`echo "$link" | sed 's/ - SE/ - S00E/'`
+ mv "$link" "$newlink"
+ done
+ #replace SyyE if no episode is in myth
+ for link in $tmprecdir/**/**/*\ -\ S[0-9][0-9]E\ -\ *
+ do
+ newlink=`echo "$link" | sed 's/E - /E00 - /'`
+ mv "$link" "$newlink"
+ done
+ #replace blank original date
+ for link in $tmprecdir/**/**/*\ -\ 0000-00-00\ -\ *
+ do
+ newlink=`echo "$link" | sed 's/ - 0000-00-00 - / - /'`
+ mv "$link" "$newlink"
+ done
+ #add dash pt suffix if filename before the subtitle is the same
+ #so that plex will scan and include in library
+ uniqs="$(ls $tmprecdir/**/**/* | sed 's/ = .*//' | sort | uniq -d)"
+ SAVEIFS=$IFS
+ IFS=$'\n'
+ for link in $uniqs
+ do
+ i=1
+ for dup in `ls -v $link*`
+ do
+ newlink=`echo "$dup" | sed "s/ = /-pt$i = /"`
+ mv "$dup" "$newlink"
+ i=$((i+1))
+ done
+ done
+ IFS=$SAVEIFS
+
+ #change symlinks mtime to match the file it is linked to
+# for link in $tmprecdir/**/*
+# do
+# if [ -L "$link" ]
+# then
+# file=`readlink "$link"`
+# touch -hr "$file" "$link"
+# fi
+# done
+
+ #sync tmprecdir to recdir
+ #rsync -aOP --delete --ignore-existing "$tmprecdir/" "$recdir/"
+ rsync -aO --delete "$tmprecdir/" "$recdir/"
+
+ #check if plex media server is running
+ if [[ `pidof "Plex Media Server"` ]]
+ then
+ #get plex section and update
+ script -q -c '/usr/bin/plexmediascanner.sh -l' | grep -i myth | cut -d: -f1 | while read -r line
+ do
+ /usr/bin/plexmediascanner.sh --scan --refresh --section $line
+ done
+ fi
+fi
+
+#END=$(date +%s)
+#DIFF=$(( $END - $START ))
+#echo "It took $DIFF seconds"
diff --git a/linhes/linhes-system/remove_storage.py b/linhes/linhes-system/remove_storage.py
new file mode 100755
index 0000000..5de2f7a
--- /dev/null
+++ b/linhes/linhes-system/remove_storage.py
@@ -0,0 +1,185 @@
+#!/usr/bin/python
+#remove_storage.py removes disks that were added using add_storage.py
+#
+#Only disks that have a conf file in /etc/storage.d/ are presented
+#as choices to remove.
+# Version 2.0.0
+
+import os,sys,subprocess
+import configparser
+from configparser import ConfigParser
+import glob
+from socket import gethostname
+from MythTV import MythDB
+
+storage_dir = "/etc/storage.d"
+
+def runcmd(cmd):
+ if True :
+ pass
+ else:
+ cmd = "echo "+cmd
+ #print cmd
+ cmdout = subprocess.getstatusoutput(cmd)
+ #logging.debug(" %s", cmdout)
+ return cmdout
+
+def read_fstab():
+ f = open('/etc/fstab', 'r')
+ fstab=f.readlines()
+ f.close()
+ return fstab
+
+def unmount_disk(conf):
+ if os.path.ismount(conf[1]):
+ print("Unmounting " + conf[1] + ".")
+ cmd = "umount %s" %conf[1]
+ runcmd(cmd)
+
+def remove_fstab_entry(conf):
+ fstab=read_fstab()
+ f = open('/etc/fstab', 'w')
+ for line in fstab:
+ if not line.startswith("#"):
+ if line.find(conf[1]) > -1 and line.find(conf[2]) > -1 :
+ print("Removing " + conf[1] + " from fstab.")
+ else:
+ f.write(line)
+ else:
+ f.write(line)
+ f.close()
+
+def remove_SG_entries(conf):
+ DB = MythDB()
+ host=gethostname()
+ with DB as c:
+ print("Removing " + conf[1] + " storage group\n paths from MythTV database.")
+ c.execute("""delete from storagegroup where hostname = %s and dirname like %s""", (host,conf[1] + '%'))
+ c.execute("""delete from settings where hostname = %s and value like %s""", (host,'SGweightPerDir:' + host + ':' + conf[1] + '%'))
+
+def remove_disk_link(conf):
+ if os.path.islink("/data/storage/disk" + str(conf[0])):
+ print("Removing link /data/storage/disk%s." %(conf[0]))
+ cmd = "rm -f /data/storage/disk%s" %(conf[0])
+ runcmd(cmd)
+
+def remove_disk_mount(conf):
+ print("Removing mountpoint %s." %(conf[1]))
+ cmd = "rm -rf %s" %(conf[1])
+ runcmd(cmd)
+
+def update_conf_file(conf):
+ print("Updating %s file." %(conf[4]))
+ parser = ConfigParser()
+ parser.read(conf[4])
+ parser.set('storage','shareable',"False")
+ parser.set('storage','removed',"True")
+ with open(conf[4], 'w') as conf[4]:
+ parser.write(conf[4])
+
+def usage():
+ help='''
+ remove_storage.py removes disks that were added using add_storage.py.
+ Only disks that have a conf file in /etc/storage.d/ are presented
+ as choices to remove.
+
+ Disks removed from the system are not erased or formatted.
+
+ Normal operations include (in this order):
+ Ask which disk to remove
+ Unmount the disk if mounted
+ Remove disk from /etc/fstab
+ Remove MythTV storage group paths in MythTV database
+ Remove disk# symlink at /data/storage/
+ Remove disk name mountpoint at /data/storage/
+ Make shareable = False in /etc/storage.d/DISKNAME.conf
+ Add removed = True in /etc/stoarge.d/DISKNAME.conf
+
+ Options:
+ -h, --help: Show this help message.
+ '''
+ print(help)
+ sys.exit(0)
+
+#--------------------------------------------
+
+def main():
+ all_confs = []
+
+ # get conf files that are not disk0 or not removed
+ for conf_file in glob.glob('%s/*.conf' %storage_dir):
+ this_conf = []
+ parser = ConfigParser()
+ parser.read(conf_file)
+ mounted = ""
+ try:
+ disk_num = parser.get('storage', 'disk_num')
+ except:
+ print("\nSkipping: " + conf_file + " is missing disk_num")
+ continue
+ try:
+ removed = parser.get('storage', 'removed')
+ except:
+ removed = False
+ if disk_num == "0" or removed:
+ continue
+ this_conf.append(int(disk_num)) #0
+ this_conf.append(parser.get('storage', 'mountpoint')) #1
+ this_conf.append(parser.get('storage', 'uuid')) #2
+ if os.path.ismount(this_conf[1]):
+ this_conf.append("(mounted)") #3
+ else:
+ this_conf.append("") #3
+ this_conf.append(conf_file) #4
+ all_confs.append(this_conf)
+
+ # sort confs on disk num
+ all_confs.sort(key=lambda x: x[0])
+
+ # exit if no disks found
+ if len(all_confs) == 0:
+ print("\nThere are no disks to remove. Exiting.")
+ sys.exit(0)
+
+ print("\nDisks found in /etc/storage.d/:\n")
+
+ # print list of disks to remove
+ for i,conf in enumerate(all_confs):
+ print(str(i+1) + ": " + conf[1] + " (disk" + str(conf[0]) + ") " + conf[3])
+
+ # get user input of disk to remove
+ try:
+ conf_select=input("\nEnter the number of the disk to remove: ")
+ conf_select=int(conf_select)
+ if conf_select > len(all_confs) or conf_select < 1:
+ conf_select=int("e")
+ except ValueError:
+ print("You must enter a number between 1 and " + str(len(all_confs)) + ". Exiting.")
+ sys.exit(0)
+
+ selected_conf = all_confs[conf_select-1]
+
+ # confirm selection
+ confirm_select=input("\nDisk " + selected_conf[1] + " (disk" + str(selected_conf[0]) + ") will be removed.\nAre you sure you want to remove it (yes/no)? ")
+ if confirm_select != "yes":
+ print("Exiting.")
+ sys.exit(0)
+
+ print("")
+ unmount_disk(selected_conf)
+ remove_fstab_entry(selected_conf)
+ remove_SG_entries(selected_conf)
+ remove_disk_link(selected_conf)
+ remove_disk_mount(selected_conf)
+ update_conf_file(selected_conf)
+
+ print("\nDisk " + selected_conf[1] + " has been removed.")
+
+if __name__ == "__main__":
+ if not os.geteuid()==0:
+ sys.exit("\nRoot access is required to run this program.\n")
+
+ if "--help" in sys.argv or "-h" in sys.argv:
+ usage()
+ else:
+ main()
diff --git a/linhes/linhes-system/rsyslog.hook b/linhes/linhes-system/rsyslog.hook
new file mode 100644
index 0000000..00e3b5f
--- /dev/null
+++ b/linhes/linhes-system/rsyslog.hook
@@ -0,0 +1,9 @@
+[Trigger]
+Operation = Install
+Type = Package
+Target = linhes-system
+
+[Action]
+Description = Enable and start rsyslog...
+When = PostTransaction
+Exec = /usr/bin/systemctl enable --now rsyslog.service
diff --git a/linhes/linhes-system/rsyslog.mythtv.conf b/linhes/linhes-system/rsyslog.mythtv.conf
new file mode 100644
index 0000000..9f3efe7
--- /dev/null
+++ b/linhes/linhes-system/rsyslog.mythtv.conf
@@ -0,0 +1,63 @@
+# MythTV 0.26 and later rsyslog.d conf
+
+# Control-code conversion. The default is on.
+#$EscapeControlCharactersOnReceive off
+
+$template DynMythFormat,"%TIMESTAMP:::date-rfc3339%%msg%\n"
+$template DynMythBackend,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythbackend.log"
+$template DynMythFrontend,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythfrontend.log"
+$template DynMythJobQueue,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythjobqueue.log"
+$template DynMythMediaServer,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythmediaserver.log"
+$template DynMythSetup,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythtv_setup.log"
+$template DynMythFillDatabase,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythfilldatabase.log"
+$template DynMythCommflag,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythcommflag.log"
+$template DynMythPreviewGen,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythpreviewgen.log"
+$template DynMythTranscode,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythtranscode.log"
+$template DynMythMetadataLookup,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythmetadatalookup.log"
+$template DynMythUtil,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythutil.log"
+$template DynMythWelcome,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythwelcome.log"
+$template DynMythShutdown,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythshutdown.log"
+$template DynMythLCDServer,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythlcdserver.log"
+$template DynMythccExtractor,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythccextractor.log"
+$template DynMythAVTest,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythavtest.log"
+$template DynMythLogServer,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_mythlogserver.log"
+$template DynMythNoLogServer,"/var/log/%$YEAR%-%$MONTH%-%$DAY%/%HOSTNAME%_%programname%.log"
+
+
+
+if $syslogfacility-text == 'local6' and $msg startswith ' mythbackend' then ?DynMythBackend;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythfrontend' then ?DynMythFrontend;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythjobqueue' then ?DynMythJobQueue;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythmediaserver' then ?DynMythMediaServer;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythtv-setup' then ?DynMythSetup;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythfilldatabase' then ?DynMythFillDatabase;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythcommflag' then ?DynMythCommflag;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythpreviewgen' then ?DynMythPreviewGen;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythtranscode' then ?DynMythTranscode;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythmetadatalookup' then ?DynMythMetadataLookup;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythutil' then ?DynMythUtil;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythwelcome' then ?DynMythWelcome;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythshutdown' then ?DynMythShutdown;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythlcdserver' then ?DynMythLCDServer;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythccextractor' then ?DynMythccExtractor;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythavtest' then ?DynMythAVTest;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $msg startswith ' mythlogserver' then ?DynMythLogServer;DynMythFormat
+& stop
+if $syslogfacility-text == 'local6' and $programname startswith 'myth' then ?DynMythNoLogServer
+& stop
diff --git a/linhes/linhes-system/system-sudo.rules b/linhes/linhes-system/system-sudo.rules
new file mode 100644
index 0000000..74ef5ef
--- /dev/null
+++ b/linhes/linhes-system/system-sudo.rules
@@ -0,0 +1,2 @@
+mythtv ALL=(ALL) NOPASSWD: ALL
+zabbix-server ALL=(ALL) NOPASSWD: /usr/bin/systemctl, /usr/bin/nmap, /usr/share/zabbix/alertscripts/*
diff --git a/linhes/linhes-system/udev_link.sh b/linhes/linhes-system/udev_link.sh
new file mode 100755
index 0000000..45038e9
--- /dev/null
+++ b/linhes/linhes-system/udev_link.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+my_base=$1
+dev_name=$2
+#DEVNAME=/dev/dvb/adapter2/frontend0
+#DVB_ADAPTER_NUM=2
+
+lndir=`dirname $dev_name`
+for clink in `ls -d /dev/dvb/adapter_*`
+do
+ if [ `readlink $clink` == $lndir ]
+ then
+ echo "link is already present $clink"
+ exit 0
+ fi
+done
+mkdir -p /dev/vstatic
+for i in 1 2 3 4
+do
+ if [ $i -eq 1 ]
+ then
+ mydir="/dev/dvb/adapter_${my_base}"
+ else
+ mydir="/dev/dvb/adapter_${my_base}-$i"
+ fi
+ if [ ! -e $mydir ]
+ then
+ ln -s $lndir $mydir
+ exit 0
+ else
+ echo "dvb $mydir already exists"
+ fi
+done
+exit 0
diff --git a/linhes/linhes-system/x11vnc.override.conf b/linhes/linhes-system/x11vnc.override.conf
new file mode 100644
index 0000000..b1d657d
--- /dev/null
+++ b/linhes/linhes-system/x11vnc.override.conf
@@ -0,0 +1,8 @@
+[Service]
+ExecStart=
+ExecStart=/bin/bash -c "/usr/bin/x11vnc -auth /var/run/sddm/* -display :0 -forever -loop -noxdamage -repeat -rfbauth /home/mythtv/.vnc/passwd -shared"
+Restart=on-failure
+RestartSec=10
+
+[Install]
+WantedBy=graphical.target
diff --git a/linhes/linhes-system/xfs_defrag.cron b/linhes/linhes-system/xfs_defrag.cron
new file mode 100644
index 0000000..208f5e8
--- /dev/null
+++ b/linhes/linhes-system/xfs_defrag.cron
@@ -0,0 +1,3 @@
+#!/bin/bash
+#. /etc/profile
+/usr/bin/checkXFSfrag.sh --idle | /usr/bin/logger -t xfs_defrag