mediainfogui 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mediainfogui-1.3.0/MANIFEST.in +2 -0
- mediainfogui-1.3.0/PKG-INFO +46 -0
- mediainfogui-1.3.0/README.md +36 -0
- mediainfogui-1.3.0/mediainfogui/MediaInfoGui.py +185 -0
- mediainfogui-1.3.0/mediainfogui/MediaInfoWidgetsGTK3.py +171 -0
- mediainfogui-1.3.0/mediainfogui/MediaInfoWidgetsGTK4.py +140 -0
- mediainfogui-1.3.0/mediainfogui/MediaInfoWidgetsQt.py +125 -0
- mediainfogui-1.3.0/mediainfogui/__init__.py +0 -0
- mediainfogui-1.3.0/mediainfogui/mediainfo.png +0 -0
- mediainfogui-1.3.0/mediainfogui.egg-info/PKG-INFO +46 -0
- mediainfogui-1.3.0/mediainfogui.egg-info/SOURCES.txt +15 -0
- mediainfogui-1.3.0/mediainfogui.egg-info/dependency_links.txt +1 -0
- mediainfogui-1.3.0/mediainfogui.egg-info/entry_points.txt +2 -0
- mediainfogui-1.3.0/mediainfogui.egg-info/requires.txt +1 -0
- mediainfogui-1.3.0/mediainfogui.egg-info/top_level.txt +1 -0
- mediainfogui-1.3.0/pyproject.toml +23 -0
- mediainfogui-1.3.0/setup.cfg +4 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mediainfogui
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: GUI for MediaInfo data - Qt6, GTK3 and GTK4
|
|
5
|
+
License-Expression: GPL-2.0-or-later
|
|
6
|
+
Project-URL: Homepage, https://github.com/kanehekili/MediaInfoGui
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: PyQt6
|
|
10
|
+
|
|
11
|
+
# MediaInfoGui
|
|
12
|
+
GUI for MediaInfo data — available in Qt6, GTK3 and GTK4
|
|
13
|
+
|
|
14
|
+
Version 1.3.0
|
|
15
|
+
|
|
16
|
+
This simple GUI for the media info binary runs on Linux. Provides a fast & clean overview of the contents of a media file.
|
|
17
|
+
|
|
18
|
+
##### Qt6 Implementation
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
##### GTK3 Implementation
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
- Opens on a given filename, usually from a file manager
|
|
28
|
+
- Shows codec data for video, audio and images
|
|
29
|
+
- Displays full MPEG-TS program structure (PAT/PMT), including broken or unresolvable programs — requires `ffprobe`
|
|
30
|
+
- Section headers highlighted with the theme's selection colour
|
|
31
|
+
- Copy to clipboard button ("Clip") for pasting media info into other applications
|
|
32
|
+
- Supports light and dark themes
|
|
33
|
+
|
|
34
|
+
### Prerequisites
|
|
35
|
+
- `mediainfo`
|
|
36
|
+
- Qt6: `python3-pyqt6` (PyQt6)
|
|
37
|
+
- GTK3: `python3-gi` with GTK 3
|
|
38
|
+
- GTK4: `python3-gi` with GTK 4
|
|
39
|
+
- Optional: `ffprobe` (from ffmpeg) for MPEG-TS program info
|
|
40
|
+
|
|
41
|
+
### How to install
|
|
42
|
+
* Download the MediaInfoGui*.tar contained in the "build" folder
|
|
43
|
+
* Unpack it and run the command **sudo ./install.sh** in the unpacked folder.
|
|
44
|
+
* Select 1 for Qt6, 2 for GTK3, or 3 for GTK4
|
|
45
|
+
* Install just copies a desktop file and some python scripts to /usr/local/sbin/MediaInfoGui
|
|
46
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# MediaInfoGui
|
|
2
|
+
GUI for MediaInfo data — available in Qt6, GTK3 and GTK4
|
|
3
|
+
|
|
4
|
+
Version 1.3.0
|
|
5
|
+
|
|
6
|
+
This simple GUI for the media info binary runs on Linux. Provides a fast & clean overview of the contents of a media file.
|
|
7
|
+
|
|
8
|
+
##### Qt6 Implementation
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
##### GTK3 Implementation
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
- Opens on a given filename, usually from a file manager
|
|
18
|
+
- Shows codec data for video, audio and images
|
|
19
|
+
- Displays full MPEG-TS program structure (PAT/PMT), including broken or unresolvable programs — requires `ffprobe`
|
|
20
|
+
- Section headers highlighted with the theme's selection colour
|
|
21
|
+
- Copy to clipboard button ("Clip") for pasting media info into other applications
|
|
22
|
+
- Supports light and dark themes
|
|
23
|
+
|
|
24
|
+
### Prerequisites
|
|
25
|
+
- `mediainfo`
|
|
26
|
+
- Qt6: `python3-pyqt6` (PyQt6)
|
|
27
|
+
- GTK3: `python3-gi` with GTK 3
|
|
28
|
+
- GTK4: `python3-gi` with GTK 4
|
|
29
|
+
- Optional: `ffprobe` (from ffmpeg) for MPEG-TS program info
|
|
30
|
+
|
|
31
|
+
### How to install
|
|
32
|
+
* Download the MediaInfoGui*.tar contained in the "build" folder
|
|
33
|
+
* Unpack it and run the command **sudo ./install.sh** in the unpacked folder.
|
|
34
|
+
* Select 1 for Qt6, 2 for GTK3, or 3 for GTK4
|
|
35
|
+
* Install just copies a desktop file and some python scripts to /usr/local/sbin/MediaInfoGui
|
|
36
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# -*- coding: iso-8859-15 -*-
|
|
2
|
+
'''
|
|
3
|
+
Created on Nov 25, 2011
|
|
4
|
+
Vern�nftige GTK Oberfl�che f�r media info
|
|
5
|
+
@author: kanehekili
|
|
6
|
+
'''
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from subprocess import Popen
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
VERSION="1.3.0"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parseLines(lines):
|
|
18
|
+
result = []
|
|
19
|
+
for line in lines:
|
|
20
|
+
row = line.decode("utf-8")
|
|
21
|
+
token = re.split('[ ]+:[ ]+', row)
|
|
22
|
+
result.append((token[0], token[1] if len(token) > 1 else ""))
|
|
23
|
+
return result
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def isHeader(row):
|
|
27
|
+
return len(row[1]) == 0 and len(row[0]) > 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def formatForClipboard(rows):
|
|
31
|
+
lines = []
|
|
32
|
+
for t0, t1 in rows:
|
|
33
|
+
if t0 and not t1:
|
|
34
|
+
lines.append(t0)
|
|
35
|
+
elif t0 or t1:
|
|
36
|
+
lines.append(f"{t0:<40} : {t1}")
|
|
37
|
+
else:
|
|
38
|
+
lines.append("")
|
|
39
|
+
return "\n".join(lines)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _isMpegTS(lines):
|
|
43
|
+
for line in lines:
|
|
44
|
+
if b"MPEG-TS" in line:
|
|
45
|
+
return True
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _streamResolvable(s):
|
|
50
|
+
ct = s.get("codec_type", "unknown")
|
|
51
|
+
if ct == "video":
|
|
52
|
+
return int(s.get("width", 0) or 0) > 0
|
|
53
|
+
if ct == "audio":
|
|
54
|
+
sr = s.get("sample_rate")
|
|
55
|
+
return sr is not None and str(sr) not in ("0", "N/A", "")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _getTSProgramLines(filename):
|
|
60
|
+
try:
|
|
61
|
+
proc = Popen(
|
|
62
|
+
["ffprobe", "-v", "quiet", "-show_programs", "-of", "json", filename],
|
|
63
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
64
|
+
)
|
|
65
|
+
out, _ = proc.communicate()
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
data = json.loads(out)
|
|
71
|
+
except ValueError:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
programs = data.get("programs", [])
|
|
75
|
+
if not programs:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
lines = []
|
|
79
|
+
lines.append(b"MPEG-TS Programs")
|
|
80
|
+
lines.append(f"{'Program count':<40} : {len(programs)}".encode())
|
|
81
|
+
|
|
82
|
+
for prog in programs:
|
|
83
|
+
prog_id = prog.get("program_id", "?")
|
|
84
|
+
pmt_pid = prog.get("pmt_pid", "?")
|
|
85
|
+
streams = prog.get("streams", [])
|
|
86
|
+
nb = len(streams)
|
|
87
|
+
|
|
88
|
+
if nb == 0:
|
|
89
|
+
value = f"PMT PID {pmt_pid} - no streams (broken)"
|
|
90
|
+
else:
|
|
91
|
+
type_counts = {}
|
|
92
|
+
for s in streams:
|
|
93
|
+
ct = s.get("codec_type", "unknown")
|
|
94
|
+
type_counts[ct] = type_counts.get(ct, 0) + 1
|
|
95
|
+
type_str = ", ".join(f"{v} {k}" for k, v in type_counts.items())
|
|
96
|
+
resolvable = any(_streamResolvable(s) for s in streams)
|
|
97
|
+
broken = "" if resolvable else " (unresolvable)"
|
|
98
|
+
value = f"PMT PID {pmt_pid} - {nb} stream{'s' if nb != 1 else ''} ({type_str}){broken}"
|
|
99
|
+
|
|
100
|
+
lines.append(f"{'Program ' + str(prog_id):<40} : {value}".encode())
|
|
101
|
+
|
|
102
|
+
lines.append(b"")
|
|
103
|
+
return lines
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _insertAfterGeneral(lines, extraLines):
|
|
107
|
+
in_general = False
|
|
108
|
+
for i, line in enumerate(lines):
|
|
109
|
+
row = line.decode("utf-8", errors="replace")
|
|
110
|
+
is_section_header = len(re.split(r'[ ]+:[ ]+', row)) == 1
|
|
111
|
+
stripped = row.strip()
|
|
112
|
+
|
|
113
|
+
if is_section_header and stripped == "General":
|
|
114
|
+
in_general = True
|
|
115
|
+
continue
|
|
116
|
+
if in_general and is_section_header:
|
|
117
|
+
if stripped == "":
|
|
118
|
+
# insert after this blank separator line
|
|
119
|
+
return lines[:i+1] + extraLines + lines[i+1:]
|
|
120
|
+
else:
|
|
121
|
+
# no blank line found before next section - insert one
|
|
122
|
+
return lines[:i] + [b""] + extraLines + lines[i:]
|
|
123
|
+
|
|
124
|
+
return lines + [b""] + extraLines
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def readMediaInfo(type,filename):
|
|
128
|
+
nameValid=False
|
|
129
|
+
if len(filename)>3:
|
|
130
|
+
result=Popen(["mediainfo",filename],stdout=subprocess.PIPE).communicate()[0]
|
|
131
|
+
nameValid= len(result) > 10
|
|
132
|
+
|
|
133
|
+
if not nameValid:
|
|
134
|
+
if type == "gtk3":
|
|
135
|
+
from . import MediaInfoWidgetsGTK3
|
|
136
|
+
MediaInfoWidgetsGTK3.showMessage("Invalid File for Media Info")
|
|
137
|
+
elif type == "gtk4":
|
|
138
|
+
from . import MediaInfoWidgetsGTK4
|
|
139
|
+
MediaInfoWidgetsGTK4.showMessage("Invalid File for Media Info")
|
|
140
|
+
else:
|
|
141
|
+
from . import MediaInfoWidgetsQt
|
|
142
|
+
MediaInfoWidgetsQt.showMessage("Invalid File for Media Info")
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
lines = result.splitlines()
|
|
146
|
+
if _isMpegTS(lines):
|
|
147
|
+
tsLines = _getTSProgramLines(filename)
|
|
148
|
+
if tsLines:
|
|
149
|
+
lines = _insertAfterGeneral(lines, tsLines)
|
|
150
|
+
|
|
151
|
+
showListDialog(type,filename,lines)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def showListDialog(type,fileName,mediaInfoList):
|
|
155
|
+
paths = fileName.split("/")
|
|
156
|
+
pLen = len(paths)
|
|
157
|
+
item = paths[pLen-2]+"/"+paths[pLen-1]
|
|
158
|
+
rows = parseLines(mediaInfoList)
|
|
159
|
+
if type == "gtk3":
|
|
160
|
+
from . import MediaInfoWidgetsGTK3
|
|
161
|
+
MediaInfoWidgetsGTK3.main([item,rows])
|
|
162
|
+
elif type == "gtk4":
|
|
163
|
+
from . import MediaInfoWidgetsGTK4
|
|
164
|
+
MediaInfoWidgetsGTK4.main([item,rows])
|
|
165
|
+
else:
|
|
166
|
+
from . import MediaInfoWidgetsQt
|
|
167
|
+
MediaInfoWidgetsQt.main([item,rows])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main(argv = None):
|
|
171
|
+
filename=""
|
|
172
|
+
type=""
|
|
173
|
+
if argv is None:
|
|
174
|
+
argv = sys.argv
|
|
175
|
+
if len(argv)>1:
|
|
176
|
+
type=argv[1]
|
|
177
|
+
if len(argv)>2:
|
|
178
|
+
filename=argv[2]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
print("Version:"+VERSION)
|
|
182
|
+
readMediaInfo(type,filename)
|
|
183
|
+
|
|
184
|
+
if __name__ == '__main__':
|
|
185
|
+
sys.exit(main())
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Created on Nov 18, 2011
|
|
3
|
+
|
|
4
|
+
@author: kanehekili
|
|
5
|
+
'''
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
import gi
|
|
9
|
+
import locale
|
|
10
|
+
gi.require_version('Gtk', '3.0')
|
|
11
|
+
from gi.repository import Gtk, Gdk,GLib,Pango
|
|
12
|
+
from .MediaInfoGui import isHeader, formatForClipboard
|
|
13
|
+
|
|
14
|
+
class MediaInfoView:
|
|
15
|
+
|
|
16
|
+
def __init__(self,fileName):
|
|
17
|
+
#create Window
|
|
18
|
+
self.window = Gtk.Window()
|
|
19
|
+
#set the window Title
|
|
20
|
+
self.window.set_title(fileName)
|
|
21
|
+
#the icon
|
|
22
|
+
homeDir = os.path.dirname(__file__)
|
|
23
|
+
appIcon = os.path.join(homeDir,"mediainfo.png")
|
|
24
|
+
self.window.set_icon_from_file(appIcon)
|
|
25
|
+
self.window.connect("delete_event",self.delete_event)
|
|
26
|
+
self.window.set_border_width(5)
|
|
27
|
+
self.window.set_size_request(500,600)
|
|
28
|
+
#create some kind of layout in which the widgets are packed
|
|
29
|
+
#layoutTable = gtk.Table(rows=2,columns=2,True)
|
|
30
|
+
layoutTable = Gtk.Table(rows=2,columns=2,homogeneous=False)
|
|
31
|
+
#TODO use buttonbox for buttons....
|
|
32
|
+
|
|
33
|
+
#create a widget the could contain the data
|
|
34
|
+
infoFrame=Gtk.Frame()
|
|
35
|
+
infoFrame.set_label("Media Info")
|
|
36
|
+
infoFrame.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
sw = self.createTextWidget()
|
|
40
|
+
sw.set_border_width(5)
|
|
41
|
+
vbox = Gtk.VBox(homogeneous=False, spacing=0)
|
|
42
|
+
vbox.pack_start(sw, expand=True, fill=True, padding=0)
|
|
43
|
+
hbox = Gtk.HBox(homogeneous=False, spacing=0)
|
|
44
|
+
hbox.pack_start(vbox, expand=True, fill=True, padding=1)
|
|
45
|
+
|
|
46
|
+
infoFrame.add(hbox)
|
|
47
|
+
|
|
48
|
+
layoutTable.attach(infoFrame,0,2,0,1,Gtk.AttachOptions.FILL| Gtk.AttachOptions.EXPAND,
|
|
49
|
+
Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND, 1, 1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
#create the button bar
|
|
53
|
+
btnBar = Gtk.HBox(homogeneous=False, spacing=0)
|
|
54
|
+
buttonCopy = Gtk.Button(label="Clip")
|
|
55
|
+
buttonCopy.connect("clicked", self.callback_copy)
|
|
56
|
+
buttonOK = Gtk.Button(label="egal", stock=Gtk.STOCK_OK)
|
|
57
|
+
buttonOK.connect("clicked", self.callback_btn_ok, "OK button")
|
|
58
|
+
btnBar.pack_start(buttonCopy, False, False, 0)
|
|
59
|
+
btnBar.pack_end(buttonOK, False, False, 0)
|
|
60
|
+
layoutTable.attach(btnBar, 0, 2, 1, 2,
|
|
61
|
+
Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
|
|
62
|
+
Gtk.AttachOptions.FILL, 1, 1)
|
|
63
|
+
#layoutTable.set_row_spacing(row=0,spacing=5)
|
|
64
|
+
self.window.add(layoutTable)
|
|
65
|
+
|
|
66
|
+
self.window.show_all()
|
|
67
|
+
sc = self.treeView.get_style_context()
|
|
68
|
+
self.header_bg_rgba = sc.get_background_color(Gtk.StateFlags.SELECTED)
|
|
69
|
+
self.header_fg_rgba = sc.get_color(Gtk.StateFlags.SELECTED)
|
|
70
|
+
|
|
71
|
+
##create a text view
|
|
72
|
+
def createTextWidget(self):
|
|
73
|
+
self.treeView = Gtk.TreeView(model=self.createTreeStore())
|
|
74
|
+
self.treeView.set_grid_lines(Gtk.TreeViewGridLines.BOTH)
|
|
75
|
+
sel = self.treeView.get_selection()
|
|
76
|
+
sel.set_mode(Gtk.SelectionMode.NONE)
|
|
77
|
+
|
|
78
|
+
#fontName="Monospace 9"
|
|
79
|
+
fontName=""
|
|
80
|
+
pangoFont = Pango.FontDescription(fontName)
|
|
81
|
+
self.treeView.modify_font(pangoFont)
|
|
82
|
+
self.createColumn(self.treeView, "Item",0)
|
|
83
|
+
self.createColumn(self.treeView, "Data",1)
|
|
84
|
+
|
|
85
|
+
sw = Gtk.ScrolledWindow()
|
|
86
|
+
#sw.set_policy(Gtk.POLICY_AUTOMATIC, Gtk.POLICY_AUTOMATIC)
|
|
87
|
+
sw.add(self.treeView)
|
|
88
|
+
return sw
|
|
89
|
+
|
|
90
|
+
def createTreeStore(self):
|
|
91
|
+
self.store = Gtk.ListStore(str,str)
|
|
92
|
+
return self.store
|
|
93
|
+
|
|
94
|
+
def _headerCellData(self, column, renderer, model, iter, col_idx):
|
|
95
|
+
is_header = isHeader((model.get_value(iter, 0), model.get_value(iter, 1)))
|
|
96
|
+
if is_header:
|
|
97
|
+
renderer.set_property('cell-background-rgba', self.header_bg_rgba)
|
|
98
|
+
renderer.set_property('cell-background-set', True)
|
|
99
|
+
renderer.set_property('foreground-rgba', self.header_fg_rgba)
|
|
100
|
+
renderer.set_property('foreground-set', True)
|
|
101
|
+
renderer.set_property('weight', 700)
|
|
102
|
+
else:
|
|
103
|
+
renderer.set_property('cell-background-set', False)
|
|
104
|
+
renderer.set_property('foreground-set', False)
|
|
105
|
+
renderer.set_property('weight', 400)
|
|
106
|
+
|
|
107
|
+
def createColumn(self,columnContainer,headerString,columnID):
|
|
108
|
+
rendererText = Gtk.CellRendererText()
|
|
109
|
+
column = Gtk.TreeViewColumn(headerString, rendererText, text=columnID)
|
|
110
|
+
column.set_cell_data_func(rendererText, self._headerCellData, columnID)
|
|
111
|
+
columnContainer.append_column(column)
|
|
112
|
+
|
|
113
|
+
def fillTable(self, rows):
|
|
114
|
+
for row in rows:
|
|
115
|
+
self.addTextLine(row)
|
|
116
|
+
|
|
117
|
+
#adds an array of strings. For the media view we need the Item name and value
|
|
118
|
+
def addTextLine(self, strings):
|
|
119
|
+
self.store.append(strings)
|
|
120
|
+
|
|
121
|
+
def openView(self):
|
|
122
|
+
Gtk.main()
|
|
123
|
+
#control returns if quit is called
|
|
124
|
+
return 0;
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ------------ Callback section -----------------
|
|
128
|
+
def callback_copy(self, widget, data=None):
|
|
129
|
+
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
|
130
|
+
clipboard.set_text(formatForClipboard([(r[0], r[1]) for r in self.store]), -1)
|
|
131
|
+
clipboard.store()
|
|
132
|
+
|
|
133
|
+
def callback_btn_ok(self, widget, data=None):
|
|
134
|
+
Gtk.main_quit();
|
|
135
|
+
|
|
136
|
+
# This callback quits the program
|
|
137
|
+
def delete_event(self, widget, event, data=None):
|
|
138
|
+
Gtk.main_quit()
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def showMessage(messageString):
|
|
143
|
+
message = Gtk.MessageDialog(None,
|
|
144
|
+
Gtk.DialogFlags.MODAL,
|
|
145
|
+
Gtk.MessageType.INFO,
|
|
146
|
+
Gtk.ButtonsType.NONE,
|
|
147
|
+
messageString)
|
|
148
|
+
message.add_button(Gtk.STOCK_QUIT, Gtk.ResponseType.CLOSE)
|
|
149
|
+
resp = message.run()
|
|
150
|
+
closewidget(message)
|
|
151
|
+
|
|
152
|
+
#hook to ensure closing widget
|
|
153
|
+
def closewidget(widget):
|
|
154
|
+
widget.destroy()
|
|
155
|
+
while Gtk.events_pending():
|
|
156
|
+
Gtk.main_iteration()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main(argv = None):
|
|
161
|
+
if argv is None:
|
|
162
|
+
argv = sys.argv
|
|
163
|
+
|
|
164
|
+
view=MediaInfoView(argv[0])
|
|
165
|
+
view.fillTable(argv[1])
|
|
166
|
+
view.openView()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == '__main__':
|
|
170
|
+
sys.exit(main())
|
|
171
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
'''
|
|
3
|
+
Created on 2025
|
|
4
|
+
|
|
5
|
+
@author: kanehekili
|
|
6
|
+
'''
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
import gi
|
|
10
|
+
gi.require_version('Gtk', '4.0')
|
|
11
|
+
from gi.repository import Gtk, Gdk, GLib, Pango
|
|
12
|
+
from .MediaInfoGui import isHeader, formatForClipboard
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MediaInfoView(Gtk.ApplicationWindow):
|
|
16
|
+
|
|
17
|
+
def __init__(self, app, fileName):
|
|
18
|
+
super().__init__(application=app, title=fileName)
|
|
19
|
+
self.set_default_size(500, 600)
|
|
20
|
+
|
|
21
|
+
# Fallback header colours — overridden once the widget is realized
|
|
22
|
+
self.header_bg_rgba = Gdk.RGBA()
|
|
23
|
+
self.header_bg_rgba.parse("#3584e4")
|
|
24
|
+
self.header_fg_rgba = Gdk.RGBA()
|
|
25
|
+
self.header_fg_rgba.parse("#ffffff")
|
|
26
|
+
self.connect("realize", self._on_realize)
|
|
27
|
+
|
|
28
|
+
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
|
29
|
+
outer.set_margin_start(5)
|
|
30
|
+
outer.set_margin_end(5)
|
|
31
|
+
outer.set_margin_top(5)
|
|
32
|
+
outer.set_margin_bottom(5)
|
|
33
|
+
|
|
34
|
+
frame = Gtk.Frame(label="Media Info")
|
|
35
|
+
frame.set_vexpand(True)
|
|
36
|
+
frame.set_child(self._buildTreeView())
|
|
37
|
+
outer.append(frame)
|
|
38
|
+
|
|
39
|
+
btn_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
40
|
+
clip_btn = Gtk.Button(label="Clip")
|
|
41
|
+
clip_btn.connect("clicked", self._on_clip)
|
|
42
|
+
ok_btn = Gtk.Button(label="OK")
|
|
43
|
+
ok_btn.connect("clicked", self._on_ok)
|
|
44
|
+
spacer = Gtk.Box()
|
|
45
|
+
spacer.set_hexpand(True)
|
|
46
|
+
btn_bar.append(clip_btn)
|
|
47
|
+
btn_bar.append(spacer)
|
|
48
|
+
btn_bar.append(ok_btn)
|
|
49
|
+
outer.append(btn_bar)
|
|
50
|
+
|
|
51
|
+
self.set_child(outer)
|
|
52
|
+
|
|
53
|
+
def _on_realize(self, widget):
|
|
54
|
+
sc = self.treeView.get_style_context()
|
|
55
|
+
found_bg, bg = sc.lookup_color("theme_selected_bg_color")
|
|
56
|
+
found_fg, fg = sc.lookup_color("theme_selected_fg_color")
|
|
57
|
+
if found_bg:
|
|
58
|
+
self.header_bg_rgba = bg
|
|
59
|
+
if found_fg:
|
|
60
|
+
self.header_fg_rgba = fg
|
|
61
|
+
self.treeView.queue_draw()
|
|
62
|
+
|
|
63
|
+
def _buildTreeView(self):
|
|
64
|
+
self.store = Gtk.ListStore(str, str)
|
|
65
|
+
self.treeView = Gtk.TreeView(model=self.store)
|
|
66
|
+
self.treeView.set_grid_lines(Gtk.TreeViewGridLines.BOTH)
|
|
67
|
+
self.treeView.get_selection().set_mode(Gtk.SelectionMode.NONE)
|
|
68
|
+
self._addColumn("Item", 0)
|
|
69
|
+
self._addColumn("Data", 1)
|
|
70
|
+
sw = Gtk.ScrolledWindow()
|
|
71
|
+
sw.set_child(self.treeView)
|
|
72
|
+
sw.set_vexpand(True)
|
|
73
|
+
return sw
|
|
74
|
+
|
|
75
|
+
def _addColumn(self, header, col_id):
|
|
76
|
+
renderer = Gtk.CellRendererText()
|
|
77
|
+
column = Gtk.TreeViewColumn(header, renderer, text=col_id)
|
|
78
|
+
column.set_cell_data_func(renderer, self._headerCellData, col_id)
|
|
79
|
+
self.treeView.append_column(column)
|
|
80
|
+
|
|
81
|
+
def _headerCellData(self, column, renderer, model, iter, col_idx):
|
|
82
|
+
is_header = isHeader((model.get_value(iter, 0), model.get_value(iter, 1)))
|
|
83
|
+
if is_header:
|
|
84
|
+
renderer.set_property('cell-background-rgba', self.header_bg_rgba)
|
|
85
|
+
renderer.set_property('cell-background-set', True)
|
|
86
|
+
renderer.set_property('foreground-rgba', self.header_fg_rgba)
|
|
87
|
+
renderer.set_property('foreground-set', True)
|
|
88
|
+
renderer.set_property('weight', 700)
|
|
89
|
+
else:
|
|
90
|
+
renderer.set_property('cell-background-set', False)
|
|
91
|
+
renderer.set_property('foreground-set', False)
|
|
92
|
+
renderer.set_property('weight', 400)
|
|
93
|
+
|
|
94
|
+
def fillTable(self, rows):
|
|
95
|
+
for row in rows:
|
|
96
|
+
self.store.append(row)
|
|
97
|
+
|
|
98
|
+
def _on_clip(self, widget):
|
|
99
|
+
text = formatForClipboard([(r[0], r[1]) for r in self.store])
|
|
100
|
+
provider = Gdk.ContentProvider.new_for_bytes(
|
|
101
|
+
"text/plain;charset=utf-8",
|
|
102
|
+
GLib.Bytes.new(text.encode("utf-8"))
|
|
103
|
+
)
|
|
104
|
+
self.get_clipboard().set_content(provider)
|
|
105
|
+
|
|
106
|
+
def _on_ok(self, widget):
|
|
107
|
+
self.get_application().quit()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def showMessage(messageString):
|
|
111
|
+
app = Gtk.Application()
|
|
112
|
+
|
|
113
|
+
def on_activate(application):
|
|
114
|
+
dialog = Gtk.MessageDialog(
|
|
115
|
+
transient_for=None,
|
|
116
|
+
message_type=Gtk.MessageType.ERROR,
|
|
117
|
+
buttons=Gtk.ButtonsType.CLOSE,
|
|
118
|
+
text=messageString
|
|
119
|
+
)
|
|
120
|
+
dialog.connect("response", lambda d, r: application.quit())
|
|
121
|
+
dialog.present()
|
|
122
|
+
|
|
123
|
+
app.connect("activate", on_activate)
|
|
124
|
+
app.run(None)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main(argv=None):
|
|
128
|
+
app = Gtk.Application()
|
|
129
|
+
|
|
130
|
+
def on_activate(application):
|
|
131
|
+
win = MediaInfoView(application, argv[0])
|
|
132
|
+
win.fillTable(argv[1])
|
|
133
|
+
win.present()
|
|
134
|
+
|
|
135
|
+
app.connect("activate", on_activate)
|
|
136
|
+
app.run(None)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == '__main__':
|
|
140
|
+
sys.exit(main())
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
'''
|
|
3
|
+
Created on May 01 2020
|
|
4
|
+
|
|
5
|
+
@author: kanehekili
|
|
6
|
+
'''
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
from PyQt6 import QtGui, QtWidgets, QtCore
|
|
10
|
+
from PyQt6.QtWidgets import QApplication, QErrorMessage, QMainWindow, QSizePolicy
|
|
11
|
+
from PyQt6.QtGui import QFont
|
|
12
|
+
from .MediaInfoGui import isHeader, formatForClipboard
|
|
13
|
+
|
|
14
|
+
class MediaInfoView(QMainWindow):
|
|
15
|
+
|
|
16
|
+
def __init__(self,fileName):
|
|
17
|
+
super(MediaInfoView,self).__init__()
|
|
18
|
+
self.initUI(fileName)
|
|
19
|
+
|
|
20
|
+
def initUI(self,fileName):
|
|
21
|
+
self.setWindowTitle(fileName)
|
|
22
|
+
palette = QApplication.palette()
|
|
23
|
+
self.hdr_bg = palette.color(QtGui.QPalette.ColorRole.Highlight)
|
|
24
|
+
self.hdr_fg = palette.color(QtGui.QPalette.ColorRole.HighlightedText)
|
|
25
|
+
|
|
26
|
+
#the icon
|
|
27
|
+
self.setWindowIcon(self.getAppIcon())
|
|
28
|
+
|
|
29
|
+
self.table = self.createListWidget()
|
|
30
|
+
clipBtn = QtWidgets.QPushButton("Clip")
|
|
31
|
+
clipBtn.clicked.connect(self.callback_copy)
|
|
32
|
+
okBtn = QtWidgets.QPushButton("OK")
|
|
33
|
+
okBtn.clicked.connect(self.callback_btn_ok)
|
|
34
|
+
|
|
35
|
+
buttonHBox = QtWidgets.QHBoxLayout()
|
|
36
|
+
buttonHBox.setContentsMargins(0, 0, 0, 0)
|
|
37
|
+
mainVBox = QtWidgets.QVBoxLayout()
|
|
38
|
+
|
|
39
|
+
buttonHBox.addWidget(clipBtn)
|
|
40
|
+
buttonHBox.addStretch()
|
|
41
|
+
buttonHBox.addWidget(okBtn)
|
|
42
|
+
|
|
43
|
+
mainVBox.addWidget(self.table)
|
|
44
|
+
mainVBox.addLayout(buttonHBox)
|
|
45
|
+
|
|
46
|
+
wid = QtWidgets.QWidget(self)
|
|
47
|
+
self.setCentralWidget(wid)
|
|
48
|
+
wid.setLayout(mainVBox)
|
|
49
|
+
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
50
|
+
self.setMinimumSize(500,600)
|
|
51
|
+
self.centerWindow()
|
|
52
|
+
|
|
53
|
+
def getAppIcon(self):
|
|
54
|
+
homeDir = os.path.dirname(__file__)
|
|
55
|
+
return QtGui.QIcon(os.path.join(homeDir,"mediainfo.png"))
|
|
56
|
+
|
|
57
|
+
def centerWindow(self):
|
|
58
|
+
screen = QApplication.primaryScreen().geometry()
|
|
59
|
+
frameGm = self.frameGeometry()
|
|
60
|
+
frameGm.moveCenter(screen.center())
|
|
61
|
+
self.move(frameGm.topLeft())
|
|
62
|
+
|
|
63
|
+
def createListWidget(self):
|
|
64
|
+
table = QtWidgets.QTableWidget()
|
|
65
|
+
font = QFont()
|
|
66
|
+
font.setPointSize(font.pointSize()-1)
|
|
67
|
+
table.setFont(font)
|
|
68
|
+
table.setColumnCount(2)
|
|
69
|
+
table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
70
|
+
table.setHorizontalHeaderLabels(["Item","Data"])
|
|
71
|
+
header = table.horizontalHeader()
|
|
72
|
+
header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
|
|
73
|
+
table.verticalHeader().setVisible(False)
|
|
74
|
+
table.verticalHeader().setStretchLastSection(True)
|
|
75
|
+
table.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.NoSelection)
|
|
76
|
+
table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
77
|
+
table.setAlternatingRowColors(True)
|
|
78
|
+
return table
|
|
79
|
+
|
|
80
|
+
def fillTable(self, rows):
|
|
81
|
+
self.rows = rows
|
|
82
|
+
for row in rows:
|
|
83
|
+
self.addTextLine(row)
|
|
84
|
+
|
|
85
|
+
def addTextLine(self, strings):
|
|
86
|
+
row= self.table.rowCount()
|
|
87
|
+
self.table.insertRow(row)
|
|
88
|
+
col=0
|
|
89
|
+
is_header = isHeader(strings)
|
|
90
|
+
for item in strings:
|
|
91
|
+
qtitem = QtWidgets.QTableWidgetItem(item)
|
|
92
|
+
if is_header:
|
|
93
|
+
if col == 0:
|
|
94
|
+
font = QFont()
|
|
95
|
+
font.setBold(True)
|
|
96
|
+
qtitem.setFont(font)
|
|
97
|
+
qtitem.setBackground(QtGui.QBrush(self.hdr_bg))
|
|
98
|
+
qtitem.setForeground(QtGui.QBrush(self.hdr_fg))
|
|
99
|
+
self.table.setItem(row,col,qtitem)
|
|
100
|
+
col=col+1
|
|
101
|
+
|
|
102
|
+
# ------------ Callback section -----------------
|
|
103
|
+
def callback_copy(self):
|
|
104
|
+
QApplication.clipboard().setText(formatForClipboard(self.rows))
|
|
105
|
+
|
|
106
|
+
def callback_btn_ok(self):
|
|
107
|
+
QApplication.quit()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def showMessage(messageString):
|
|
111
|
+
app = QtWidgets.QApplication([])
|
|
112
|
+
msg = QErrorMessage()
|
|
113
|
+
msg.showMessage(messageString)
|
|
114
|
+
app.exec()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main(argv = None):
|
|
118
|
+
app=QApplication(sys.argv)
|
|
119
|
+
view=MediaInfoView(argv[0])
|
|
120
|
+
view.fillTable(argv[1])
|
|
121
|
+
view.show()
|
|
122
|
+
app.exec()
|
|
123
|
+
|
|
124
|
+
if __name__ == '__main__':
|
|
125
|
+
sys.exit(main())
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mediainfogui
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: GUI for MediaInfo data - Qt6, GTK3 and GTK4
|
|
5
|
+
License-Expression: GPL-2.0-or-later
|
|
6
|
+
Project-URL: Homepage, https://github.com/kanehekili/MediaInfoGui
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: PyQt6
|
|
10
|
+
|
|
11
|
+
# MediaInfoGui
|
|
12
|
+
GUI for MediaInfo data — available in Qt6, GTK3 and GTK4
|
|
13
|
+
|
|
14
|
+
Version 1.3.0
|
|
15
|
+
|
|
16
|
+
This simple GUI for the media info binary runs on Linux. Provides a fast & clean overview of the contents of a media file.
|
|
17
|
+
|
|
18
|
+
##### Qt6 Implementation
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
##### GTK3 Implementation
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
- Opens on a given filename, usually from a file manager
|
|
28
|
+
- Shows codec data for video, audio and images
|
|
29
|
+
- Displays full MPEG-TS program structure (PAT/PMT), including broken or unresolvable programs — requires `ffprobe`
|
|
30
|
+
- Section headers highlighted with the theme's selection colour
|
|
31
|
+
- Copy to clipboard button ("Clip") for pasting media info into other applications
|
|
32
|
+
- Supports light and dark themes
|
|
33
|
+
|
|
34
|
+
### Prerequisites
|
|
35
|
+
- `mediainfo`
|
|
36
|
+
- Qt6: `python3-pyqt6` (PyQt6)
|
|
37
|
+
- GTK3: `python3-gi` with GTK 3
|
|
38
|
+
- GTK4: `python3-gi` with GTK 4
|
|
39
|
+
- Optional: `ffprobe` (from ffmpeg) for MPEG-TS program info
|
|
40
|
+
|
|
41
|
+
### How to install
|
|
42
|
+
* Download the MediaInfoGui*.tar contained in the "build" folder
|
|
43
|
+
* Unpack it and run the command **sudo ./install.sh** in the unpacked folder.
|
|
44
|
+
* Select 1 for Qt6, 2 for GTK3, or 3 for GTK4
|
|
45
|
+
* Install just copies a desktop file and some python scripts to /usr/local/sbin/MediaInfoGui
|
|
46
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
mediainfogui/MediaInfoGui.py
|
|
5
|
+
mediainfogui/MediaInfoWidgetsGTK3.py
|
|
6
|
+
mediainfogui/MediaInfoWidgetsGTK4.py
|
|
7
|
+
mediainfogui/MediaInfoWidgetsQt.py
|
|
8
|
+
mediainfogui/__init__.py
|
|
9
|
+
mediainfogui/mediainfo.png
|
|
10
|
+
mediainfogui.egg-info/PKG-INFO
|
|
11
|
+
mediainfogui.egg-info/SOURCES.txt
|
|
12
|
+
mediainfogui.egg-info/dependency_links.txt
|
|
13
|
+
mediainfogui.egg-info/entry_points.txt
|
|
14
|
+
mediainfogui.egg-info/requires.txt
|
|
15
|
+
mediainfogui.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PyQt6
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mediainfogui
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mediainfogui"
|
|
7
|
+
version = "1.3.0"
|
|
8
|
+
description = "GUI for MediaInfo data - Qt6, GTK3 and GTK4"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "GPL-2.0-or-later"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = ["PyQt6"]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://github.com/kanehekili/MediaInfoGui"
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
mediainfogui = "mediainfogui.MediaInfoGui:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.package-data]
|
|
21
|
+
mediainfogui = [
|
|
22
|
+
"*.png",
|
|
23
|
+
]
|