Содержание

Разработка

ScreenGen

Последняя версия - 1.14 от 03.08.2023: Исходники

readme.md

Creates the list thumbnails of the video file in console. Based on ffmpeg and QT. About 60 parameters for setting.

Last compile on Fedora 41 with ffmpeg 7.0.2 and QT5.15.15-1.

Example video - https://youtu.be/FGfhkvHVTcQ

Parameters:
 --preset                  Use section in config file (default - "Default")
 --presetsList             Show all presets in config file (all other parameters ignored)
 --presetInfo              Show all values in preset (all other parameters ignored, example: --presetInfo MyPreset)
 --frames                  Frames count in screenlist (default - 16)
 --width                   Width screenlist (default - 1200)
 --horCount                Frames in horisontal (default - 4)
 --offsetBegin             Time offset from begin (default - "00:01:00")
 --offsetEnd               Time offset from end (default - "00:01:00")
 --listBorder              Depth border screenlist (default - 10)
 --frameBorder             Depth border frame (default - 5)
 --shadowOffset            Offset shadow of frame (default - 5)
 --shadowInt               Intensity shadow of frame (default - 10, range 0-100)
 --shadowColor             R,G,B color shadow of frame (default - "0,0,0" - black, "255,255,255" - white)
 --background              Picture file for background (if empty, fill white)
 --backgroundColor         R,G,B[,A] background color (default - "255,255,255")
 --picType                 Type of picture file (default - "jpg")
 --header                  Header with information (default - true)
 --timeFrame               Time stamp on frame (default - true)
 --fontFrameName           Name font on frame (default - "Liberation Sans")
 --fontFrameSize           Size font on frame (default - 10)
 --fontFrameWeight         Weight font on frame (default - Normal, (Light,DemiBold,Bold,Black))
 --fontFrameItalic         Italic font on frame (default - false)
 --fontFrameColor          R,G,B[,A] color font on frame (default - "255,255,255")
 --fontFrameShadowOffset   Offset shadow of time stamp (default - 0)
 --fontFrameShadowInt      Intensity shadow of time stamp (default - 2, range 0-100)
 --fontFrameShadowColor    R,G,B color shadow of time stamp (default - "0,0,0")
 --stampPos                Position time stamp (default - "rb" (RightBottom), (rb, rt, lb, lr))
 --stampOffset             Offset from the edge of the frame to the timestamp (default - 3)
 --stampStart              Time offset for timestamp (default - "00:00:00")
 --fontHeaderName          Name font on header (default - "Liberation Sans")
 --fontHeaderSize          Size font on header (default - 13)
 --fontHeaderWeight        Weight font on header (default - Bold, (Normal,Light,DemiBold,Black))
 --fontHeaderItalic        Italic font on header (default - false)
 --fontHeaderColor         R,G,B[,A] color font on header (default - "0,0,0")                                                                       
 --headerBackground        Picture file for header background (if empty, default color for header)                                                  
 --headerColor             R,G,B[,A] color header background (default - "255,255,255")                                                              
 --headerBorderColor       R,G,B[,A] color header border (default - "0,0,0")                                                                        
 --headerBorder            Depth border header (default - 2)                                                                                        
 --infoShadowOffset        Offset shadow of information text (default - 5)                                                                          
 --infoShadowInt           Intensity shadow of information text (default - 10, range 0-100)                                                         
 --infoShadowColor         R,G,B color shadow of information text (default - "100,100,100")                                                         
 --logoFile                Picture file for logotype (it is recommended PNG image with alpha channel)                                               
 --logoShadowOffset        Offset shadow of logotype (default - 5)                                                                                  
 --logoShadowInt           Intensity shadow of logotype (default - 10, range 0-100)
 --logoShadowColor         R,G,B color shadow of logotype (default - "100,100,100")
 --foreground              Picture file for foreground (it is recommended PNG image with alpha channel)
 --aspect                  Aspect ratio (if 0.0 - source aspect (DAR), default - 0.0)
 --descr                   Custom text, located at the top of the header
 --outFolder               Folder for result files
 --frameBoxColor           R,G,B[,A] color frame border (default - "120,120,120")
 --frameBox                Depth border frame (default - 0)
 --stampDescr              Custom text, located on frame
 --stampDescrPos           Position custom text (default - "lt" (LeftTop), (rb, rt, lb, lr))
 --timeStep                Time between frames. If not equal to "00:00:00", the parameter "frames" are ignored. (default - "00:00:00")
 --fpm                     Frames per minute. If not equal to "0", the parameter "frames" and "timeStep" are ignored. (default - "0")
 --maxRows                 Maximum rows if use "timeStep" or "fpm". (default - 50)
 --version                 Version string. (default - false)
 --logLevel                FFMPEG log level. (default - -8 (QUIET))

CHANGELOG

2012-08-15 (1.0)
* First release.

2012-08-16 (1.1)
* Change backend from xine-lib to ffmpeg-libs.

2012-08-16 (1.2)
* Fix errors, clean code.
* Added parameter "stampPos".
* Delete option --video.
* Added the ability translate header.
* Added russian translate for header.

2012-08-23 (1.3)
* Fix errors.
* Change algorithm shadow create.
* Added parameters "infoShadowOffset", "infoShadowInt", "infoShadowColor" for text in header.
* Added parameters "logoFile", "logoShadowOffset", "logoShadowInt", logoShadowColor" for logotype in header.
* Added parameter "descr" - Custom text, located at the top of the header.
* Added parameter "stampOffset" - Offset from the edge of the frame to the timestamp.

2012-08-23 (1.3.2)
* Added parameter "outFolder".
* Change logic for save output file - saved in current folder or in folder from parameter "outFolder".

2012-08-31 (1.4)
* Disable log messages from ffmpeg-libs.
* Added parameters "fontFrameShadowOffset", "fontFrameShadowInt", "fontFrameShadowColor" for timestamp shadow.

2012-09-19 (1.5)
* Fix errors.
* Added parameters "frameBox", "frameBoxColor", "stampDescr", "stampDescrPos".
* All color parameters (except shadows) support alpha channel (maybe set to "R,G,B,A").

2012-11-22 (1.6)
* Fix segmentation fault if audio not present in video file.
* Added parameters "presetsList" and "presetInfo".

2013-09-01 (1.7)
* Fix segmentation fault if open error videofile, if not exists video stream or video codec.
* Video Codec names fixed.
* Added parameter "timeStep".

2014-09-09 (1.8)
* Change console output.
* Change default aspect ratio to DAR (Display Aspect Ratio).
* Added parameter "maxRows" if use parameter "timeStep".
* Added parameter "version".
* Added parameter "stampStart".
* Added example scripts.

2015-08-03 (1.9)
* Fixed deprecated call of FFMpeg (2.4.3).

2017-09-12 (1.10)
* Fixed deprecated call of libAVCodec 57.63.103.
* Fixed for H264 codec.
* Added parameter (from console only) "logLevel" - FFMpeg log. Available:
   -8  QUIET (default)
    0  PANIC
    8  FATAL
   16  ERROR
   24  WARNING
   32  INFO
   40  VERBOSE
   48  DEBUG
   56  TRACE
* Added video bitrate info if available.

2019-02-17 (1.11)
* Fixed offsetBegin and offsetEnd parameters

2023-08-02 (1.12)
* Fixed to ffmpeg 6.0

2023-08-03 (1.13)
* Add --fpm parameter

2023-08-03 (1.14)
* Fix errors

Спасибо всем, кто разместил информацию о программе:

https://youtu.be/FGfhkvHVTcQ
https://github.com/Svarkovsky/screengen
https://www.debugpoint.com/create-video-thumbnail-image-using-screengen-in-ubuntu/
https://zenway.ru/page/screengen
https://linuxaria.com/pills/screengen-how-to-easily-create-a-thumbnail-from-a-video-with-linux
https://astapm.github.io/soft/screengen.html

IFaces

Показывает трафик на сервере.
https://kochkin.tk/ifaces/

Исходник

<!doctype html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Скорость на интерфейсах</title>
<script src="raphael-min.js" type="text/javascript" charset="utf-8"></script>

</head>
<body>

<script>

// By Oleg Kochkin 2010. <kochkin.tk>
// https://github.com/OlegKochkin/ifaces
// Данное ПО распространяется под лицензией GPL
//
// Использованы:
//
// - Библиотека Raphaël для JavaScript. Copyright (c) 2008 - 2010 Dmitry Baranovskiy <raphaeljs.com>
// - Части кода примеров Raphaël. Copyright (c) 2008 - 2010 Dmitry Baranovskiy <raphaeljs.com>
// - Пример по Ajax - http://forum.eugen.su/showthread.php?t=132&p=599

function createRequest() {
	if (request != null) return;
	try {
		request = new XMLHttpRequest();
	} catch (failed) {
		request = null;
	}
	if (request == null) alert("Error creating request object!");
}

// Посылка запроса на сервер
function getIfacesSpeed() {
    createRequest();
    var url = "getIfacesSpeed.php";
    request.onreadystatechange = viewSpeed();
    request.open("GET", url, true);
    request.send(null);
}

function paintIndicator(nc, left, top, nameText, scaleIndex, diameter, width, border){
	var scale=ind[nc].scaleValuesDigits[scaleIndex];
	var valueText=ind[nc].scaleValuesText[scaleIndex];
	ind[nc].currentIdScale=scaleIndex;
	ind[nc].scale=scale;
	ind[nc].diameter=diameter;	// Диаметр
	ind[nc].width=width;
	ind[nc].border=border;
	ind[nc].digits = [];
	ind[nc].scale10 = [];
	ind[nc].scale5 = [];
	ind[nc].max=0;
	ind[nc].min=10;

var r2=ind[nc].diameter/2,
	r1=r2-r2/20,
	r5=r2-r2/3,
	r4=r2-r2/13.33,
	r3=r2-r2/6.67,
	r5=r2-r2/3;
	cx=r2+width+border,
	cy=r2+width+border,
	beta=2*Math.PI/130,
	pathParams={stroke: "#0f0","stroke-width": width,"stroke-linecap": "round"};
	ind[nc].canvas=Raphael (left, top, r2*2+width*2+border*2, r2*2+width*2+border*2);

// Подложка
bg1 = ind[nc].canvas.circle(r2+width+border, r2+width+border, r2+width+border-4);
bg1.attr("fill", "black");
// Центр стрелки
bg2 = ind[nc].canvas.circle(r2+width+border, r2+width+border, r2/10);
bg2.attr("fill", "blue");
for (var i=0; i<130; i++){
	var alpha=beta*i-Math.PI/2,
		cos=Math.cos(alpha),
		sin=Math.sin(alpha);
	if (i==52){
// Индикатор переполнения
		ind[nc].over = ind[nc].canvas.circle(cx+r1*cos,cy+r1*sin, r2/20);
		ind[nc].over.attr({gradient: "300-#fff-#f00:50-#900"});
		}
// Мелкие риски шкалы
	if (i<50 || i>80){
			var sector=ind[nc].canvas.path ("M"+(cx+r1*cos)+" "+(cy+r1*sin)+"L"+(cx+r2*cos)+" "+(cy+r2*sin)).attr (pathParams);
		}
	}
// Единицы измерения
ind[nc].valueTextObj=ind[nc].canvas.text (r2+ind[nc].width+ind[nc].border, r2+r2/2.86, valueText);
ind[nc].valueTextObj.attr ({fill: "gray", "font-size": r2/9, "font-style": "italic", "font-weight": "bold"});

for (var j=-5;j<=5;j++){
	var beta2=2*Math.PI/13,
		alpha2=beta2*j-Math.PI/2,
		cos2=Math.cos(alpha2),
		sin2=Math.sin(alpha2),
		dig=(j+5)*scale/10;
// Цифры
 		ind[nc].digits[j+5]=ind[nc].canvas.text (cx+r5*cos2,cy+r5*sin2,dig+"");
 		ind[nc].digits[j+5].attr ({fill: "#0ff", "font-size": r2/8.33, "font-weight": "bold"});
// Риски шкалы 1/10
		ind[nc].scale10[j+5]=ind[nc].canvas.path ("M"+(cx+r3*cos2)+" "+(cy+r3*sin2)+"L"+(cx+r2*cos2)+" "+(cy+r2*sin2)).attr ({stroke: "yellow","stroke-width": r2/40,"stroke-linecap": "round"})
// Риски шкалы 1/5
	if (j<5){
		var alpha3=beta2*(j+0.5)-Math.PI/2,
		cos3=Math.cos(alpha3),
		sin3=Math.sin(alpha3);
		ind[nc].scale5[j+5]=ind[nc].canvas.path ("M"+(cx+r4*cos3)+" "+(cy+r4*sin3)+"L"+(cx+r2*cos3)+" "+(cy+r2*sin3)).attr({stroke: "yellow","stroke-width": r2/66.66,"stroke-linecap": "round"})
		}
	}
// Сообщение
ind[nc].message=ind[nc].canvas.text (r2+width+border, r2+r2/1.2, "Запуск");
ind[nc].message.attr ({fill: "red", "font-size": r2/9, "font-style": "italic", "font-weight": "bold"});
ind[nc].message.animate ({opacity: 0},3000);

setScale (nc);

// Название индикатора
var nameTextIn=ind[nc].canvas.text (r2+width+border, r2-r2/4, nameText);
nameTextIn.attr ({fill: "gray", "font-size": r2/9, "font-style": "italic", "font-weight": "bold"});
// Стрелка
ind[nc].arrow=ind[nc].canvas.path ("M"+(r2+width+border)+" "+(r2+width+border+r2/6.5)+"L"+(r2+width+border)+" "+(r2-r2/1.35)).attr({stroke: "white","stroke-width": r2/28.5,"stroke-linecap": "round"})
// Верхушка центра стрелки
bg3 = ind[nc].canvas.circle(r2+width+border, r2+width+border, r2/20);
bg3.attr("fill", "red");
// Стекло
bg4 = ind[nc].canvas.image("glass.png", 0, 0, (r2+width+border)*2, (r2+width+border)*2);
// Поворот стрелки в нулевое положение
ind[nc].arrow.rotate (360*8/13, r2+width+border, r2+width+border);
var angle=360*8/13;
ind[nc].arrow.animate({rotation: [angle, ind[nc].diameter/2+ind[nc].width+ind[nc].border, ind[nc].diameter/2+ind[nc].width+ind[nc].border]}, 10);
}

function setScale (nc){
var scale=ind[nc].scaleValuesDigits[ind[nc].currentIdScale];
var valueText=ind[nc].scaleValuesText[ind[nc].currentIdScale];
ind[nc].scale=scale;
ind[nc].valueText=valueText;
ind[nc].valueTextObj.attr ("text",valueText);
for (var j=-5;j<=5;j++){
	var dig=(j+5)*scale/10;
	dig=dig+"";
	ind[nc].digits[j+5].attr ("text", dig);
	}
ind[nc].message.attr ({"opacity": 1, "text": "Смена шкалы"});
ind[nc].message.animate ({opacity: 0},5000);
}

function arrowRotate (nc, aind){
	var sc=ind[nc].scaleValues[ind[nc].currentIdScale];
	if (ind[nc].firstTeek){
		var curSc=0;
		for (var i=0;i<ind[nc].scaleValues.length;i++){
			curSc=ind[nc].scaleValues[i];
			if (aind<curSc){
				ind[nc].currentIdScale=i;
				sc=curSc;
				setScale (nc);
				ind[nc].firstTeek=false;
				break;
				}
			}
		}
// Если переполнение -> включить индикатор и "упереть" стрелку
		if (aind>sc){
// Если значение больше шкалы
		aind=ind[nc].scale;
		ind[nc].over.attr({gradient: "300-#fff-#f00:50-#900"});
		ind[nc].max++;
		if (ind[nc].max>scaleChangeTics){
// Если значение зашкаливает scaleChangeTics тиков подряд
			ind[nc].currentIdScale++;
			if (ind[nc].currentIdScale > ind[nc].scaleValues.length-1) ind[nc].currentIdScale = ind[nc].scaleValues.length-1;
			else setScale (nc);
			ind[nc].max=0;
			}
		}
	else {
// Если значение меньше шкалы
		ind[nc].over.attr({gradient: "300-#666-#600:50-#300"});
		ind[nc].max--;
		if (ind[nc].max<0) ind[nc].max=0;
		}
	if (aind<sc/10){
// Если значение меньше 1/10 шкалы
		ind[nc].min--;
		if (ind[nc].min<0){
			ind[nc].currentIdScale--;
			if (ind[nc].currentIdScale < 0) ind[nc].currentIdScale = 0;
			else setScale (nc);
			ind[nc].min=scaleChangeTics;
			}
		}
	else{
		ind[nc].min++;
		if (ind[nc].min>scaleChangeTics) ind[nc].min=scaleChangeTics;
		}
// Пересчёт килобайтов в градусы
    var angle=360*80/130+aind*360/(sc+sc*3/10);
// Поворот стрелки на нужный угол
	ind[nc].arrow.animate({rotation: [angle, ind[nc].diameter/2+ind[nc].width+ind[nc].border, ind[nc].diameter/2+ind[nc].width+ind[nc].border]}, 5000);
}

// При получении данных от сервера
function viewSpeed() {
	if (request.readyState == 4) {
		var str=request.responseText;

// Получаемые данные:
		values=str.split ("|");

        current_rx = values[0] - prev_rx;
        prev_rx = values[0];
        current_tx = values[1] - prev_tx;
        prev_tx = values[1];

// alert (current_tx);
 		arrowRotate ("IFaceRx", Number(current_rx / 2048));
 		arrowRotate ("IFaceTx", Number(current_tx / 2048));

		request = null;
	}
}

var ind=[];
var request = null;
var scaleChangeTics=5;

var prev_rx = 0, prev_tx = 0, current_rx = 0, current_tx = 0;

window.onload = function () {

var vaules=[];
ind["default"]=[];
ind["default"].scaleValuesDigits=[1,10,20,50,100,200,500,2000,10,20];
ind["default"].scaleValues=[1,10,20,50,100,200,500,2048, 10240,20480];
ind["default"].scaleValuesText=["КБайт/сек","КБайт/сек","КБайт/сек","КБайт/сек","КБайт/сек","КБайт/сек","КБайт/сек","КБайт/сек","МБайт/сек","МБайт/сек"];
ind["default"].firstTeek=true;

ind["IFaceRx"]=[];
ind["IFaceTx"]=[];

// Шкалы
ind["IFaceRx"].scaleValuesDigits=ind["default"].scaleValuesDigits.slice()
ind["IFaceRx"].scaleValues=ind["default"].scaleValues.slice();
ind["IFaceRx"].scaleValuesText=ind["default"].scaleValuesText.slice();
ind["IFaceRx"].firstTeek=ind["default"].firstTeek;

ind["IFaceTx"].scaleValuesDigits=ind["default"].scaleValuesDigits.slice()
ind["IFaceTx"].scaleValues=ind["default"].scaleValues.slice();
ind["IFaceTx"].scaleValuesText=ind["default"].scaleValuesText.slice();
ind["IFaceTx"].firstTeek=ind["default"].firstTeek;

var cols=2,diam=400;
var px = (window.outerWidth - (diam*cols+10)) / 2;
var py = (window.outerHeight - (11+diam)) / 2;
var bg=Raphael (px, py, diam*cols+10, 11+diam);
var bgrect=bg.rect (4, 5, cols*diam+1, diam, 20);

bgrect.attr ("fill", "black");
paintIndicator ("IFaceRx", px+10, py+10, "Приём", 5, diam-30, 2, 7);
paintIndicator ("IFaceTx", px+diam+10, py+10, "Передача", 5, diam-30, 2, 7);

// Таймер. Каждые 1000 мс запускает getIfacesSpeed()
var timerGetSpeed=setInterval ("getIfacesSpeed();",1000);
}

</script>
</body>
</html>

TVOk

Простой IPTV плеер с источниками потоков в m3u файлах.

  • Поддерживает работу с несколькими m3u файлами.
  • Управляется с помощью DBUS, что позволяет использовать ИК пульт.

Исходник

#!/usr/bin/python3

# Simple IPTV player for sources in m3u files
# TVOK. Version 0.6.3 (27.07.2023). By Oleg Kochkin. License GPL.

import vlc
import sys, os, time, xml.etree.ElementTree as ET, datetime, textwrap
from PyQt5.QtWidgets import QApplication,QWidget,QMainWindow,QMenu,QAction,QLabel,QSystemTrayIcon,QFrame,QGridLayout,QBoxLayout
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QSettings, Qt, pyqtSlot, QTimer
from PyQt5.QtDBus import QDBusConnection, QDBusMessage, QDBusInterface, QDBusReply, QDBus

WINDOW_DECORATION_HEIGHT_TITLE = 25
WINDOW_DECORATION_WIDTH_BORDER = 4
VOLUME_CHANGE=5
VOLUME_LEVEL=80

# Get folder with script
scriptDir = os.path.dirname(os.path.realpath(__file__))
# Config file init 
cfg = QSettings('tvok','tvok')
# Load playlist
pl = []

try:
	list = sys.argv[1]
except:
	try:
		list = scriptDir+os.path.sep+'tvok.m3u'
	except:
		print("Run example:\n\ttvok.py ChannelsFile.m3u")
		exit(1)

f = open(list)
line = f.readline()
while line:
	if "#EXTINF:" in line:
		tvg=''
		if ('tvg-id=' in line): tvg=line.split('tvg-id="')[1].split('"')[0]
		ch = line.split(',')[1].strip()
		url = f.readline().strip()
		pl.append([ch,url,tvg])
	line = f.readline()
f.close()

class MainWindow(QMainWindow):
	def __init__(self):
		super(MainWindow, self).__init__()
	def createUI(self):
		self.home = os.getenv("HOME")
# vlc player init
		self.instance = vlc.Instance('--ipv4-timeout=1000 --config='+self.home+'/.config/tvok/vlcrc -q')
		self.mediaplayer = self.instance.media_player_new()
# Main window settings    
		self.gridLayout = QGridLayout(self)
		self.gridLayout.setObjectName("gridLayout")
		self.videoFrame = QFrame(self)
		self.videoFrame.setObjectName("videoFrame")
		self.gridLayout.addWidget(self.videoFrame, 0, 0, 1, 1)

		self.mediaplayer.set_xwindow(int(self.winId()))

		self.setWindowIcon(QIcon(scriptDir+os.path.sep+'pics'+os.path.sep+'logo.png'))
		self.resize(cfg.value('Width',456,type=int),cfg.value('Height',256,type=int))
		self.setGeometry(cfg.value(os.path.basename(list)+'/Left',456,type=int)+WINDOW_DECORATION_WIDTH_BORDER,
										cfg.value(os.path.basename(list)+'/Top',256,type=int)+WINDOW_DECORATION_HEIGHT_TITLE,
										cfg.value(os.path.basename(list)+'/Width',456,type=int),
										cfg.value(os.path.basename(list)+'/Height',256,type=int))
		pal = self.palette()
		pal.setColor(self.backgroundRole(), Qt.darkBlue)
		self.setPalette(pal)
		self.currentCursor = self.cursor()
# Save status audio mute
		self.AudioMuteOnStart = self.mediaplayer.audio_get_mute()
		self.AudioVolumeOnStart = self.mediaplayer.audio_get_volume()
		self.Volume = cfg.value('Volume',VOLUME_LEVEL,type=int)
		self.EpgInMenu = cfg.value(os.path.basename(list)+'/EpgInMenu',False,type=bool)
		self.EPGOk = True
		try:
			self.EPG = ET.parse(scriptDir+os.path.sep+'epg.xml')
		except:
			self.EPGOk = False

# Registered DBUS service
		DBUSName = 'tv.ok'
		DBUSConn = QDBusConnection.connectToBus(QDBusConnection.SessionBus, DBUSName)
		DBUSConn.registerService(DBUSName)
		DBUSConn.registerObject("/", self, QDBusConnection.ExportAllContents)
		
		self.ChMenu = QMenu()
		self.ChMenu.setToolTipsVisible(True)
		self.ChMenu.setStyleSheet("font:14pt")
		self.ChMenu.setToolTipDuration(1)
		for chs in pl:
			action = self.ChMenu.addAction(chs[0])
		self.ChMenu.addSeparator()
		self.quitAction = self.ChMenu.addAction(self.tr("Quit"))

# Timer 1 second init. Once second call function t1secEvent
		self.t1sec = QTimer(self)
		self.t1sec.timeout.connect(self.t1secEvent)
		self.t1sec.start(1000)
		self.t1min = QTimer(self)
		self.t1min.timeout.connect(self.t1minEvent)
		self.t1min.start(60000)

		self.trayIcon = QSystemTrayIcon()
		self.trayIcon.setToolTip('TVOK '+os.path.basename(list))
		self.trayIcon.activated.connect (self.ToggleMute)
#		print ("MUTE:",self.mediaplayer.audio_get_mute())
		self.swapIcon()

# Select channel saved previous run
		self.chNum = cfg.value(os.path.basename(list)+"/Channel",1,type=int)
		self.chPrev = self.chNum + 1
		self.chChange()
		
		self.selectChannel = ''
		self.tChSelect = QTimer(self)
		self.tChSelect.timeout.connect(self.tChSelectTimeout)

		self.tLongStartJob = QTimer(self)
		self.tLongStartJob.timeout.connect(self.ChMenuToolTipRefresh)
		self.tLongStartJob.start(1)

	def ChMenuToolTipRefresh(self):
		self.tLongStartJob.stop()
		for index,chs in enumerate(pl):
			self.ChMenu.actions()[index].setToolTip(chs[0]+'\n'+self.GetChannelProg(index))

	def osdView(self,mess):
# Send OSD
# If DBUS daemon org.kochkin.okindd is running
		dbus_interface = QDBusInterface("org.kochkin.okindd", "/Text")
		if dbus_interface.isValid():
			dbus_interface.call('printText', 'Tvok', mess, 5000)

	@pyqtSlot(int)
	def channelNum(self,digit):
		if (digit >= 0) and (digit <= 9):
			self.selectChannel = self.selectChannel+str(digit)
			if int(self.selectChannel) > len(pl): self.selectChannel = self.selectChannel[:-1]
			if int(self.selectChannel) < 1: self.selectChannel = self.selectChannel[:-1]
			self.osdView(self.selectChannel+': '+pl[int(self.selectChannel)-1][0])
			self.tChSelect.start(2000)

	@pyqtSlot()
	def tChSelectTimeout(self):
		self.tChSelect.stop()
		self.chNum = int (self.selectChannel)
		self.selectChannel = ''
		self.chChange()

	def swapIcon(self):
		picture = scriptDir+os.path.sep+'pics'+os.path.sep+'din-on.png'
		if not self.mute(): picture = scriptDir+os.path.sep+'pics'+os.path.sep+'din-off.png'
		self.trayIcon.setIcon (QIcon (picture))
		self.trayIcon.show()

	@pyqtSlot(result=bool)
	def mute(self): return self.mediaplayer.audio_get_mute()

	@pyqtSlot(result=int)
	def GetChannelNum(self): return self.chNum

	@pyqtSlot(result=str)
	def GetChannelName(self): return pl[self.chNum-1][0]

	@pyqtSlot(int,result=str)
	def GetChannelProg(self,chNumber):
		Title = ""
		if (self.EPGOk):
			rootEPG = self.EPG.getroot()
			ChName = pl[chNumber][0]
			import cr
			if (ChName in cr.ChReplace): ChName = cr.ChReplace[ChName]
#			print(ChName)
			Now = (datetime.datetime.now()).strftime("%Y%m%d%H%M%S")
			ChId=pl[chNumber][2]
			for prog in rootEPG.findall("./programme/[@channel='"+ChId+"']"):
				Start=prog.get('start').split()[0]
				Stop=prog.get('stop').split()[0]
				if ((Start < Now) and (Stop > Now)):
					Title=prog.find('title').text.split(' [')[0]
		return Title

	@pyqtSlot(result=int)
	def GetVolume(self): return self.mediaplayer.audio_get_volume()
	@pyqtSlot()
	
	def VolumeIncrease(self):
		self.mediaplayer.audio_set_volume(self.mediaplayer.audio_get_volume()+VOLUME_CHANGE)
	@pyqtSlot()
	
	def VolumeDecrease(self):
		self.mediaplayer.audio_set_volume(self.mediaplayer.audio_get_volume()-VOLUME_CHANGE)

# Once second
	def t1secEvent(self):
		if self.isFullScreen():
			self.CursorOff()

# Once minute
	def t1minEvent(self):
		ChPr=self.GetChannelProg(self.chNum-1)
		DelimWin=". "
		DelimOsd="\n"
		if (ChPr == ""):
			DelimWin=""
			DelimOsd=""
		NewWindowTitle = str(self.chNum)+". "+pl[self.chNum-1][0]+DelimWin+ChPr
		if (NewWindowTitle != self.windowTitle()):
			print(self.windowTitle()+" -> "+NewWindowTitle)
			self.setWindowTitle(NewWindowTitle)
			self.osdView(str(self.chNum)+': '+pl[self.chNum-1][0]+DelimOsd+textwrap.fill(ChPr,40))
		self.tLongStartJob.start(1)

	@pyqtSlot()
	def ToggleMute(self):
		self.mediaplayer.audio_set_mute(not self.mediaplayer.audio_get_mute())
		cfg.setValue(os.path.basename(list)+'/AudioMute',self.mute())
		self.swapIcon()

	@pyqtSlot()
	def ChannelNext(self):
		self.chNum += 1
		self.chChange()
		
	@pyqtSlot()
	def ChannelPrev(self):
		self.chNum -= 1
		self.chChange()

	def keyPressEvent(self,event):
		if event.key() == Qt.Key_PageUp: self.ChannelNext()
		if event.key() == Qt.Key_PageDown: self.ChannelPrev()
		event.accept()

# On mouse wheel change    
	def wheelEvent(self,event):
		if event.angleDelta().y() > 0: self.ChannelNext()
		if event.angleDelta().y() < 0: self.ChannelPrev()
		event.accept()

	@pyqtSlot()
	def ChannelRestart(self): self.chChange()

	@pyqtSlot(result=str)
	def getCrop(self):
		return self.mediaplayer.video_get_crop_geometry()

	@pyqtSlot(str)
	def setCrop(self,crop):
		self.mediaplayer.video_set_crop_geometry(crop)
		cfg.setValue(self.GetChannelName()+'/Crop',crop)
		cfg.sync()

	@pyqtSlot(result=str)
	def getAspect(self):
		return self.mediaplayer.video_get_aspect_ratio()

	@pyqtSlot(str)
	def setAspect(self,aspect):
		self.mediaplayer.video_set_aspect_ratio(aspect)

	@pyqtSlot()
	def chReconnect(self):
		self.chPrev = self.chNum + 1
		self.chChange()

# Stop current channel and start chNum channel
	def chChange(self):
		if self.chNum != self.chPrev:
			if self.chNum > len(pl): self.chNum = 1
			if self.chNum < 1: self.chNum = len(pl)
			ChPr=self.GetChannelProg(self.chNum-1)
			DelimWin=". "
			DelimOsd="\n"
			if (ChPr == ""):
				DelimWin=""
				DelimOsd=""
			self.setWindowTitle(str(self.chNum)+'. '+pl[self.chNum-1][0]+DelimWin+ChPr)
			self.osdView(str(self.chNum)+': '+pl[self.chNum-1][0]+DelimOsd+textwrap.fill(ChPr,40))
			self.mediaplayer.stop()

			self.media = self.instance.media_new(pl[self.chNum-1][1])
			self.mediaplayer.set_media(self.media)
			playerError = self.mediaplayer.play()
			self.setVolume = QTimer(self)
			self.setVolume.timeout.connect(self.setVolumeMax)
			self.setVolume.start()
#			print ("playerError = "+str(playerError))
			if playerError != 0: sys.exit()
			cfg.setValue('Channel',self.chNum)
			cfg.setValue(os.path.basename(list)+"/Channel",self.chNum)
			self.chPrev = self.chNum
			if self.isFullScreen():
				self.CursorOff()

	def setVolumeMax(self):
			self.mediaplayer.audio_set_volume(VOLUME_LEVEL)
			if self.mediaplayer.audio_get_volume() >= VOLUME_LEVEL: self.setVolume.stop()
	
# If double click left button mouse - toggle full screen    
	def mouseDoubleClickEvent(self,event):
		if event.button() == Qt.MouseButton.LeftButton: self.ToggleFullScreen()
		event.accept()

# If middle click mouse - toggle audio mute    
	def mousePressEvent(self,event):
		if event.button() == Qt.MouseButton.MiddleButton: self.ToggleMute()
		event.accept()

	def CursorOff(self):
			self.setCursor(Qt.BlankCursor)
			QApplication.setOverrideCursor(Qt.BlankCursor)

	def CursorOn(self):
			self.setCursor(self.currentCursor)
			QApplication.setOverrideCursor(self.currentCursor)

	@pyqtSlot()
	def ToggleFullScreen(self):
		if self.isFullScreen():
			self.showNormal()
			self.CursorOn()
		else:
			self.showFullScreen()
			self.CursorOff()

# Mouse pressed for context menu
	def contextMenuEvent(self,event):
		self.CursorOn()
		action = self.ChMenu.exec_(self.mapToGlobal(event.pos()))
		if action: 
			value_index=1
			for value in pl:
				if value[0] == action.iconText():
					if self.chNum != value_index:
						self.chNum = value_index
						self.chChange()
					break
				else: value_index += 1
			if action == self.quitAction:
				self.close()
		event.accept()

	def closeEvent(self, event):
		self.mediaplayer.stop()
		if not self.isFullScreen():
			cfg.setValue(os.path.basename(list)+'/Left',self.x())
			cfg.setValue(os.path.basename(list)+'/Top',self.y())
			cfg.setValue(os.path.basename(list)+'/Width',self.width())
			cfg.setValue(os.path.basename(list)+'/Height',self.height())
		cfg.sync()
		cfg.setValue(os.path.basename(list)+'/AudioMute',self.mute())
		self.mediaplayer.audio_set_mute(self.AudioMuteOnStart)
		event.accept()
		exit()
		
app = QApplication(sys.argv)
tvok = MainWindow()
tvok.createUI()
tvok.show()
sys.exit(app.exec_())

Наверх
uptime