diff options
Diffstat (limited to 'linhes/linhes-system')
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 |