├── qtefun ├── 组件 │ ├── __init__.py │ ├── 系统托盘图标.py │ ├── 菜单栏.py │ ├── 组件汉化基类.py │ ├── 单选框.py │ ├── 按钮.py │ ├── 标签.py │ ├── 菜单.py │ ├── 容器.py │ ├── 复选框.py │ ├── 组合框.py │ ├── 单行编辑框.py │ ├── 工具条.py │ ├── 纯文本编辑框.py │ ├── 富文本编辑框.py │ ├── 列表框.py │ ├── 表格.py │ ├── 树形框.py │ └── 组件公共类.py ├── __init__.py ├── .gitignore ├── 图标.py ├── 国际化.py ├── 全局热键.py ├── 公共函数.py ├── 组件使用例子 │ ├── 3.单选框组件.py │ ├── 1.标签组件.py │ ├── 2.复选框组件.py │ ├── 6.表格.py │ ├── 7.容器.py │ ├── 4.列表框.py │ └── 5.树形框.py ├── 消息通信.py ├── README.md └── 部件公共类.py ├── version.py ├── go2tv_res ├── .gitignore ├── cmd │ ├── go2tv │ │ └── version.txt │ └── go2tv-lite │ │ ├── version.txt │ │ └── go2tv.go ├── assets │ ├── go2tv-icon.png │ ├── go2tv-icon.svg │ └── go2tv-red.svg ├── utils │ ├── random_test.go │ ├── urlencoder.go │ ├── random.go │ ├── urlencoder_test.go │ ├── urlstreamer.go │ ├── transcode.go │ ├── transcode_windows.go │ ├── iptools.go │ ├── iptools_test.go │ └── dlnatools.go ├── Dockerfile ├── Makefile ├── soapcalls │ ├── friendlyname.go │ ├── friendlyname_test.go │ ├── xmlparsers_test.go │ └── xmlparsers.go ├── LICENSE ├── internal │ ├── gui │ │ ├── layouts.go │ │ ├── settings.go │ │ ├── about.go │ │ ├── about_mobile.go │ │ ├── gui_mobile.go │ │ ├── gui.go │ │ └── main_mobile.go │ └── interactive │ │ └── interactive.go ├── go.mod ├── devices │ └── devices.go └── README.md ├── app.ico ├── app.png ├── app.icns ├── go2tv ├── go2tv └── go2tv.exe ├── requirements.txt ├── app.qrc ├── .gitmodules ├── images └── README │ ├── 2022-07-31_12.40.17.png │ └── 2022-07-31_12.42.45.png ├── .gitignore ├── nanodlna ├── __init__.py ├── templates │ ├── action-Pause.xml │ ├── action-Stop.xml │ ├── action-Play.xml │ ├── action-SetAVTransportURI.xml │ ├── metadata-video_subtitle.xml │ └── metadata-example1.xml ├── dlna.py ├── streaming.py ├── devices.py └── cli.py ├── 文件服务类.py ├── 文件服务器.py ├── .github ├── release-drafter.yml └── workflows │ └── 发布软件.yml ├── run_write_version.py ├── README.md ├── 投屏模块.py ├── go2tv模块.py ├── ui_多多投屏.ui ├── ui_多多投屏.py └── easy_to_tv.py /qtefun/组件/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | version = "1.0" -------------------------------------------------------------------------------- /go2tv_res/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /go2tv_res/cmd/go2tv/version.txt: -------------------------------------------------------------------------------- 1 | 1.12.0 2 | -------------------------------------------------------------------------------- /go2tv_res/cmd/go2tv-lite/version.txt: -------------------------------------------------------------------------------- 1 | 1.12.0 2 | -------------------------------------------------------------------------------- /qtefun/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .部件公共类 import * 3 | 4 | -------------------------------------------------------------------------------- /app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolabmeng6/easy_to_tv/HEAD/app.ico -------------------------------------------------------------------------------- /app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolabmeng6/easy_to_tv/HEAD/app.png -------------------------------------------------------------------------------- /app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolabmeng6/easy_to_tv/HEAD/app.icns -------------------------------------------------------------------------------- /go2tv/go2tv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolabmeng6/easy_to_tv/HEAD/go2tv/go2tv -------------------------------------------------------------------------------- /go2tv/go2tv.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolabmeng6/easy_to_tv/HEAD/go2tv/go2tv.exe -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PySide6==6.3.1 2 | QtAwesome==1.1.1 3 | pyinstaller 4 | icecream 5 | flask 6 | pyefun -------------------------------------------------------------------------------- /app.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | app.png 4 | 5 | 6 | -------------------------------------------------------------------------------- /go2tv_res/assets/go2tv-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolabmeng6/easy_to_tv/HEAD/go2tv_res/assets/go2tv-icon.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "qtAutoUpdateApp"] 2 | path = qtAutoUpdateApp 3 | url = https://github.com/duolabmeng6/qtAutoUpdateApp 4 | -------------------------------------------------------------------------------- /images/README/2022-07-31_12.40.17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolabmeng6/easy_to_tv/HEAD/images/README/2022-07-31_12.40.17.png -------------------------------------------------------------------------------- /images/README/2022-07-31_12.42.45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolabmeng6/easy_to_tv/HEAD/images/README/2022-07-31_12.42.45.png -------------------------------------------------------------------------------- /qtefun/.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /__pycache__ 3 | /build 4 | /dist 5 | /docs/build 6 | __pycache__ 7 | /venv 8 | /cache/ 9 | *_test.rst 10 | .idea -------------------------------------------------------------------------------- /qtefun/图标.py: -------------------------------------------------------------------------------- 1 | # 图标库 2 | # 安装 pip install qtawesome 3 | # qta-browser 图标浏览器 4 | 5 | import qtawesome 6 | 7 | 8 | def 获取图标(名字, 颜色): 9 | return qtawesome.icon(名字, color=颜色) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /__pycache__ 3 | /build 4 | /dist 5 | /docs/build 6 | __pycache__ 7 | /venv 8 | /cache/ 9 | *_test.rst 10 | .idea 11 | /o 12 | /build 13 | /go2tvproject 14 | *.mp4 15 | 16 | -------------------------------------------------------------------------------- /nanodlna/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __title__ = 'nanodlna' 4 | __version__ = '0.2.1' 5 | __short_version__ = '.'.join(__version__.split('.')[:2]) 6 | __author__ = 'Gabriel Magno' 7 | __license__ = 'MIT' 8 | __copyright__ = 'Copyright 2016, Gabriel Magno' 9 | -------------------------------------------------------------------------------- /go2tv_res/utils/random_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRandomString(t *testing.T) { 8 | _, err := RandomString() 9 | if err != nil { 10 | t.Fatalf("RandomString: failed to test due to error %s", err.Error()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /go2tv_res/utils/urlencoder.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/url" 5 | "path" 6 | "strings" 7 | ) 8 | 9 | // ConvertFilename is a helper function that percent-encodes a string. 10 | func ConvertFilename(s string) string { 11 | out := url.QueryEscape(path.Base(s)) 12 | out = strings.ReplaceAll(out, "+", "%20") 13 | return out 14 | } 15 | -------------------------------------------------------------------------------- /文件服务类.py: -------------------------------------------------------------------------------- 1 | # 定义一个键值对储存文件与路径的关系 2 | 3 | 文件与路径 = {} 4 | 5 | 6 | def 写文件名与路径(文件名, 路径): 7 | 文件与路径.update({文件名: 路径}) 8 | 9 | 10 | def 取路径(文件名): 11 | return 文件与路径.get(文件名) 12 | 13 | def 清空(): 14 | 文件与路径.clear() 15 | 16 | 17 | if __name__ == '__main__': 18 | pass 19 | 写文件名与路径("aaa", "/aaaaa/bbbb.mp4") 20 | print(取路径("aaa")) 21 | -------------------------------------------------------------------------------- /nanodlna/templates/action-Pause.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 0 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /nanodlna/templates/action-Stop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 0 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /go2tv_res/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17.1-bullseye 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | RUN \ 5 | apt-get update && \ 6 | apt-get install -y xorg-dev && \ 7 | apt-get clean && \ 8 | rm -rf /var/lib/apt/lists/* 9 | 10 | WORKDIR /usr/local/src/go2tv/ 11 | COPY . . 12 | 13 | ENV GODEBUG=asyncpreemptoff=1 14 | RUN make 15 | RUN make install 16 | 17 | ENTRYPOINT [ "go2tv" ] 18 | -------------------------------------------------------------------------------- /qtefun/国际化.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Signal, QLocale, QTranslator 2 | from PySide6.QtCore import QObject 3 | 4 | def 设置语言为中文(): 5 | # 设置系统语言为中文 6 | # qt_zh_CN.qm 需要翻译文件 7 | # 使用方法 app.installTranslator(设置语言为中文()) 8 | qLocale = QLocale(QLocale.Chinese, QLocale.SimplifiedChineseScript) 9 | trans = QTranslator() 10 | trans.load("qt_zh_CN") 11 | return trans 12 | -------------------------------------------------------------------------------- /nanodlna/templates/action-Play.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 0 6 | 1 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /go2tv_res/Makefile: -------------------------------------------------------------------------------- 1 | LDFLAGS="-s -w" 2 | 3 | build: clean 4 | go build -ldflags $(LDFLAGS) -o build/go2tv cmd/go2tv-lite/go2tv.go 5 | 6 | install: 7 | go build -ldflags -s -w -o build/go2tv cmd/go2tv-lite/go2tv.go 8 | mkdir -vp /usr/local/bin/ 9 | #cp build/go2tv /usr/local/bin/ 10 | cp build/go2tv /Users/chensuilong/Desktop/pythonproject/easy_to_tv 11 | 12 | 13 | uninstall: 14 | rm -vf /usr/local/bin/go2tv 15 | 16 | clean: 17 | rm -rf ./build 18 | -------------------------------------------------------------------------------- /qtefun/组件/系统托盘图标.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QSystemTrayIcon 2 | 3 | 4 | class 系统托盘图标(QSystemTrayIcon): 5 | def __init__(self, parent=None): 6 | super().__init__(parent) 7 | 8 | def 设置托盘菜单(self, 菜单): 9 | self.setContextMenu(菜单) 10 | 11 | def 设置托盘图标(self, icon): 12 | self.setIcon(icon) 13 | 14 | def 设置提示文本(self, 提示文本): 15 | self.setToolTip(提示文本) 16 | 17 | def 显示(self): 18 | self.show() 19 | 20 | def 隐藏(self): 21 | self.hide() -------------------------------------------------------------------------------- /go2tv_res/utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | ) 7 | 8 | // RandomString generates a random string which we 9 | // the callback paths for our webservers. 10 | func RandomString() (string, error) { 11 | b := make([]byte, 16) 12 | n, err := rand.Read(b) 13 | if err != nil { 14 | if n > 0 { 15 | return fmt.Sprintf("%X", b), nil 16 | } 17 | return "", fmt.Errorf("can't generate a random number: %w", err) 18 | } 19 | return fmt.Sprintf("%X", b), nil 20 | } 21 | -------------------------------------------------------------------------------- /qtefun/组件/菜单栏.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QMenu, QMenuBar 2 | 3 | 4 | class 菜单栏(QMenuBar): 5 | parent = None 6 | 7 | def __init__(self, parent=None, 对象名称=None): 8 | super().__init__(parent) 9 | self.parent = parent 10 | if 对象名称: 11 | self.setObjectName(对象名称) 12 | 13 | def 添加项目(self, 菜单): 14 | self.addAction(菜单) 15 | 16 | def 设置位置(self, rect): 17 | # self.菜单栏.设置位置(QRect(0, 0, 563, 24)) # 设置菜单栏位置和大小 18 | self.setGeometry(rect) 19 | -------------------------------------------------------------------------------- /nanodlna/templates/action-SetAVTransportURI.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 0 6 | {uri_video} 7 | {metadata} 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /qtefun/全局热键.py: -------------------------------------------------------------------------------- 1 | # 用于绑定全局热键 2 | # 需要安装 pynput 3 | # pip install pynput 4 | from pynput import keyboard 5 | 6 | 热键和函数的映射 = {} 7 | 8 | 9 | def 注册全局热键(欲绑定的按键, 回调函数): 10 | """ 11 | 注册全局热键 12 | :param 欲绑定的按键: 按键名称 +1 13 | :param 回调函数: 回调函数 14 | :return: 15 | """ 16 | 热键和函数的映射[欲绑定的按键] = 回调函数 17 | 18 | 19 | def 开启全局热键(): 20 | with keyboard.GlobalHotKeys(热键和函数的映射) as h: 21 | h.join() 22 | 23 | 24 | if __name__ == '__main__': 25 | def 按下Ctrl_1(): 26 | print('ctrl+1 按下') 27 | 28 | 29 | def 按下Ctrl_2(): 30 | print('ctrl+2 按下') 31 | 32 | 33 | 注册全局热键('+1', 按下Ctrl_1) 34 | 注册全局热键('+2', 按下Ctrl_2) 35 | 开启全局热键() 36 | -------------------------------------------------------------------------------- /文件服务器.py: -------------------------------------------------------------------------------- 1 | from urllib import parse 2 | 3 | from flask import Flask, send_file 4 | 5 | import 文件服务类 6 | 7 | app = Flask(__name__) 8 | 9 | 10 | @app.route('/') 11 | def mp4(url_path): 12 | print("url_path", url_path) 13 | url_path = parse.quote(url_path) 14 | path = 文件服务类.取路径(url_path) 15 | # 检查path的文件是否存在 16 | if not path: 17 | return "文件不存在" 18 | print("path", path) 19 | 20 | return send_file(path, as_attachment=False) 21 | 22 | 23 | if __name__ == '__main__': 24 | print("123") 25 | 文件服务类.写文件名与路径(r"1.mp4", r"C:\Users\csuil\Desktop\华为手机视频\VID_20201213_102126.mp4") 26 | app.run(host='0.0.0.0', port=6161, threaded=True, debug=True) 27 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🚀 新功能' 5 | labels: 6 | - '新功能' 7 | - title: '🐛 Bug 修复' 8 | labels: 9 | - 'bug' 10 | - title: '🧰 日常维护' 11 | label: '日常维护' 12 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 13 | change-title-escapes: '\<*_&' 14 | version-resolver: 15 | major: 16 | labels: 17 | - 'major' 18 | minor: 19 | labels: 20 | - 'minor' 21 | patch: 22 | labels: 23 | - 'patch' 24 | default: patch 25 | template: | 26 | # 多多投屏 27 | * 轻轻松松从MacOS 和 Window中投视频到电视上 28 | * 永久免费 29 | 30 | $CHANGES 31 | no-changes-template: | 32 | 快下载体验~ 33 | -------------------------------------------------------------------------------- /go2tv_res/utils/urlencoder_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCovertFilename(t *testing.T) { 8 | tt := []struct { 9 | name string 10 | input string 11 | want string 12 | }{ 13 | { 14 | `Test #1`, 15 | `something & something.mp4`, 16 | `something%20%26%20something.mp4`, 17 | }, 18 | { 19 | `Test #2`, 20 | `something + something.mp4`, 21 | `something%20%2B%20something.mp4`, 22 | }, 23 | } 24 | 25 | for _, tc := range tt { 26 | t.Run(tc.name, func(t *testing.T) { 27 | out := ConvertFilename(tc.input) 28 | if out != tc.want { 29 | t.Fatalf("%s: got: %s, want: %s.", tc.name, out, tc.want) 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /run_write_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | def 查看系统所有环境变量(): 5 | from icecream import ic 6 | # 打印系统所有的环境变量 7 | for item in os.environ: 8 | name = item 9 | value = os.environ[item] 10 | ic(name, value) 11 | # 查看系统所有环境变量() 12 | version = os.environ.get('version') 13 | print("version:", version) 14 | # 获取运行目录 15 | # print("run dir:", os.getcwd()) 16 | # 获取文件目录 17 | 文件目录 = os.path.dirname(__file__) 18 | print("file dir:", os.path.dirname(__file__)) 19 | versionFilePath = os.path.join(文件目录, "version.py") 20 | print("edit file {versionFilePath} output: version = {version}") 21 | # 覆盖写出文件 version.py 中 22 | with open(versionFilePath, 'w') as f: 23 | f.write(f'version = "{version}"') 24 | 25 | exit() 26 | -------------------------------------------------------------------------------- /qtefun/公共函数.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import datetime 3 | 4 | from PySide6.QtCore import QCoreApplication 5 | from PySide6.QtWidgets import QApplication 6 | from qtpy.uic import loadUi 7 | 8 | def 异常检测(function): 9 | def box(*args, **kwargs): 10 | try: 11 | return function(*args, **kwargs) 12 | except: 13 | print(function.__name__, "函数发生异常") 14 | print("错误发生时间:", str(datetime.datetime.now())) 15 | print("错误的详细情况:", traceback.format_exc()) 16 | 17 | return box 18 | 19 | 20 | def 加载ui文件(ui文件名,容器=None): 21 | return loadUi(ui文件名, 容器) 22 | 23 | def 应用退出(): 24 | QCoreApplication.quit() 25 | 26 | def 设置关闭窗口不退出(): 27 | QApplication.setQuitOnLastWindowClosed(False) # 关闭最后一个窗口不退出程序 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 新开发的多多投屏 支持进度条 音量控制 2 | 3 | 新项目地址 https://github.com/duolabmeng6/projection_screen_tv 4 | 5 | 前往下载 https://github.com/duolabmeng6/projection_screen_tv/releases 6 | 7 | 8 | # 下面是旧的内容 9 | # 多多投屏 永久免费 开源软件 10 | 11 | 轻轻松松在MacOS和Window中将视频文件投屏到电视上,跟手机的投屏功能一致,无需nas等繁琐操作.直接文件投屏. 12 | 13 | # MacOS 14 | ![image-20220730180009303](images/README/2022-07-31_12.42.45.png) 15 | # Window 16 | ![image-20220730180009303](images/README/2022-07-31_12.40.17.png) 17 | 18 | # 软件下载 19 | 20 | [最新版下载](https://github.com/duolabmeng6/easy_to_tv/releases) 21 | 22 | # 运行环境 23 | 24 | * Window10 25 | * MacOS 26 | * ~win7~ (由于采用最新的qt6技术所以不支持win7) 27 | 28 | 电视中需要安装接收端例如 29 | 30 | * 当贝投屏 无广告 31 | * ~乐播投屏~ 投屏后有广告恶心人 32 | * 银河奇异果 无广告 33 | 34 | 35 | # 赞赏 36 | 37 | 如果觉得项目对你有帮助,可以请作者喝杯咖啡 38 | -------------------------------------------------------------------------------- /qtefun/组件/组件汉化基类.py: -------------------------------------------------------------------------------- 1 | """ 2 | 组件汉化基类 3 | 4 | 使得原有对象可以轻松的汉化 5 | 6 | 由于猴子补丁的模式,导致ide没有中文命令提示,所以采用该方式 7 | 8 | 封装组件时继承该类即可 9 | """ 10 | from PySide6.QtWidgets import QWidget 11 | 12 | 13 | class 组件汉化基类(object): 14 | 对象 = QWidget 15 | 16 | def __init__(self, 对象): 17 | self.对象 = 对象 18 | 19 | def __getattr__(self, 方法名): 20 | # 将调用不存在的方法转移到对象中 兼容原来的写法 21 | def 检查方法是否存在(*args, **kwargs): 22 | # 调用对象中的方法 23 | if 方法名 in dir(self.对象): 24 | return getattr(self.对象, 方法名)(*args, **kwargs) 25 | # print(f'调用方法不存在 方法名:{方法名} 参数为:{args}, {kwargs}') 26 | raise Exception(f'调用方法不存在 方法名:{方法名} 参数为:{args}, {kwargs}') 27 | 28 | if 方法名 in dir(self): 29 | return getattr(self, 方法名) 30 | return 检查方法是否存在 31 | -------------------------------------------------------------------------------- /qtefun/组件/单选框.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | 3 | from qtefun.组件.组件公共类 import 组件公共类 4 | 5 | # https://doc.qt.io/qt-5/qcheckbox.html 6 | 7 | class 单选框(组件公共类): 8 | 对象 = None # type: QtWidgets.QRadioButton 9 | 10 | # 获取标题 11 | @property 12 | def 标题(self): 13 | return self.对象.text() 14 | 15 | # 设置标题 16 | @标题.setter 17 | def 标题(self, value: str): 18 | return self.对象.setText(value) 19 | 20 | # 设置选中属性 21 | @property 22 | def 选中(self): 23 | return self.对象.isChecked() 24 | 25 | # 设置选中属性 26 | @选中.setter 27 | def 选中(self, value: bool): 28 | return self.对象.setChecked(value) 29 | 30 | # 绑定事件 单选框点击事件 完成一次鼠标点击就会触发 31 | def 绑定事件被点击(self, 回调函数): 32 | self.对象.clicked.connect(回调函数) 33 | 34 | # 绑定事件 单选框选中状态切换 选中 = True 选中的状态改变时就会触发 35 | def 绑定事件选中状态切换(self, 回调函数): 36 | """ 37 | 回调函数(选中状态: bool) 38 | """ 39 | self.对象.toggled.connect(回调函数) 40 | -------------------------------------------------------------------------------- /go2tv_res/utils/urlstreamer.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | ) 11 | 12 | // StreamURL returns the response body for the input media URL. 13 | func StreamURL(ctx context.Context, s string) (io.ReadCloser, error) { 14 | _, err := url.ParseRequestURI(s) 15 | if err != nil { 16 | return nil, fmt.Errorf("streamURL failed to parse url: %w", err) 17 | } 18 | 19 | client := &http.Client{} 20 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, s, nil) 21 | if err != nil { 22 | return nil, fmt.Errorf("streamURL failed to call NewRequest: %w", err) 23 | } 24 | 25 | resp, err := client.Do(req) 26 | if err != nil { 27 | return nil, fmt.Errorf("streamURL failed to client.Do: %w", err) 28 | } 29 | 30 | if resp.StatusCode >= 400 { 31 | return nil, errors.New("streamURL bad status code: " + resp.Status) 32 | } 33 | 34 | body := resp.Body 35 | 36 | return body, nil 37 | } 38 | -------------------------------------------------------------------------------- /qtefun/组件使用例子/3.单选框组件.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide6.QtWidgets import * 4 | 5 | from qtefun.组件.单选框 import 单选框 6 | 7 | 8 | class Main(QMainWindow): 9 | def __init__(self): 10 | super(Main, self).__init__() 11 | self.init_ui() 12 | 13 | def init_ui(self): 14 | self.resize(400, 400) 15 | self.setWindowTitle("单选框组件学习") 16 | self.show() 17 | 18 | self.单选框 = QRadioButton(self) 19 | self.单选框.move(50, 50) 20 | self.单选框.setText("Hello World!") 21 | self.单选框.show() 22 | 23 | self.单选框1 = 单选框(self.单选框) 24 | self.单选框1.标题 = "祖国您好!" 25 | self.单选框1.选中 = True 26 | self.单选框1.绑定事件选中状态切换(self.选中状态切换) 27 | self.单选框1.绑定事件被点击(self.绑定事件被点击) 28 | 29 | def 选中状态切换(self, 选中状态): 30 | print("选中状态切换", 选中状态) 31 | 32 | 33 | def 绑定事件被点击(self): 34 | print("绑定事件被点击") 35 | 36 | 37 | app = QApplication([]) 38 | # 创建窗口 400x400 39 | win = Main() 40 | 41 | sys.exit(app.exec()) 42 | -------------------------------------------------------------------------------- /nanodlna/templates/metadata-video_subtitle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | nano-dlna Video 4 | {uri_video} 5 | {uri_sub} 6 | {uri_sub} 7 | {uri_sub} 8 | {uri_sub} 9 | object.item.videoItem.movie 10 | 11 | 12 | -------------------------------------------------------------------------------- /go2tv_res/soapcalls/friendlyname.go: -------------------------------------------------------------------------------- 1 | package soapcalls 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // GetFriendlyName returns the friendly name value for a the specific DMR url. 10 | func GetFriendlyName(dmr string) (string, error) { 11 | client := &http.Client{} 12 | req, err := http.NewRequest(http.MethodGet, dmr, nil) 13 | if err != nil { 14 | return "", fmt.Errorf("failed to create NewRequest for GetFriendlyName: %w", err) 15 | } 16 | 17 | req.Header.Set("Connection", "close") 18 | 19 | resp, err := client.Do(req) 20 | if err != nil { 21 | return "", fmt.Errorf("failed to send HTTP request for GetFriendlyName: %w", err) 22 | } 23 | defer resp.Body.Close() 24 | 25 | var fn struct { 26 | FriendlyName string `xml:"device>friendlyName"` 27 | } 28 | 29 | if err = xml.NewDecoder(resp.Body).Decode(&fn); err != nil { 30 | return "", fmt.Errorf("failed to read response body for GetFriendlyName: %w", err) 31 | } 32 | 33 | return fn.FriendlyName, nil 34 | } 35 | -------------------------------------------------------------------------------- /qtefun/消息通信.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Signal 2 | from PySide6.QtCore import QObject 3 | 4 | 5 | class 消息通信(QObject): 6 | """ 7 | 消息通信类 8 | 9 | 例如: 10 | # 主窗口 main.py 定义 11 | 主窗口信号 = 消息通信(str) 12 | 13 | # 接收消息 14 | self.主窗口信号.接收消息(self.win_login.父窗口消息) 15 | self.win_login.子窗口信号.接收消息(self.子窗口消息) 16 | 17 | # 定义接收函数 18 | def 子窗口消息(self, 消息内容): 19 | print("子窗口消息", 消息内容) 20 | 21 | # 发送消息 22 | self.主窗口信号.发送消息("发送给子窗口") 23 | 24 | # 子窗口 win_login.py 定义 25 | 子窗口信号 = 消息通信(str) 26 | 27 | # 定义接收函数 28 | def 父窗口消息(self, 消息内容): 29 | print("父窗口消息",消息内容) 30 | 31 | # 发送消息 32 | self.子窗口信号.发送消息("发送给主窗口") 33 | 34 | 35 | """ 36 | 信号 = Signal(object) 37 | 38 | def __init__(self): 39 | super(消息通信, self).__init__() 40 | 41 | def 接收消息(self, 函数): 42 | self.信号.connect(函数) 43 | 44 | def 发送消息(self, param): 45 | pass 46 | self.信号.emit(param) 47 | -------------------------------------------------------------------------------- /qtefun/组件使用例子/1.标签组件.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide6.QtWidgets import QApplication, QMainWindow, QLabel 4 | 5 | from qtefun.组件.标签 import 标签 6 | 7 | 8 | class Main(QMainWindow): 9 | def __init__(self): 10 | super(Main, self).__init__() 11 | self.init_ui() 12 | 13 | def init_ui(self): 14 | self.resize(400, 400) 15 | self.setWindowTitle("标签组件学习") 16 | self.show() 17 | 18 | # 创建标签 19 | self.label = QLabel(self) 20 | # 设置标签的位置 21 | self.label.move(50, 50) 22 | # 设置标签的文本 23 | self.label.setText("Hello World!") 24 | # 显示标签 25 | self.label.show() 26 | 27 | self.标签1 = 标签(self.label) 28 | self.标签1.标题 = "祖国您好!" 29 | self.标签1.绑定事件被按下(self.点击事件) 30 | self.标签1.绑定事件被松开(self.松开事件) 31 | 32 | def 点击事件(self,e): 33 | print("点击事件") 34 | 35 | 36 | def 松开事件(self,e): 37 | print("松开事件") 38 | 39 | app = QApplication([]) 40 | # 创建窗口 400x400 41 | win = Main() 42 | 43 | 44 | 45 | sys.exit(app.exec()) 46 | -------------------------------------------------------------------------------- /qtefun/组件/按钮.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | 3 | from qtefun.组件.组件公共类 import 组件公共类 4 | 5 | 6 | class 按钮(组件公共类): 7 | """ 8 | 绑定事件方法 9 | 被点击事件 clicked() 10 | 被按下事件 pressed() 11 | 被松开事件 released() 12 | 选中状态切换事件 toggled() 13 | 14 | """ 15 | 对象 = None # type: QtWidgets.QPushButton 16 | 17 | # 获取标题 18 | @property 19 | def 标题(self): 20 | return self.对象.text() 21 | 22 | # 设置标题 23 | @标题.setter 24 | def 标题(self, value: str): 25 | print("设置标题", value) 26 | return self.对象.setText(value) 27 | 28 | # 绑定事件 按钮点击事件 完成一次鼠标点击就会触发 29 | def 绑定事件被点击(self, 回调函数): 30 | self.对象.clicked.connect(回调函数) 31 | 32 | # 绑定事件 鼠标按下事件 一直按住不松开就会触发 33 | def 绑定事件被按下(self, 回调函数): 34 | self.对象.pressed.connect(回调函数) 35 | 36 | # 绑定事件 鼠标抬起事件 按住按住后松开就会触发 37 | def 绑定事件被松开(self, 回调函数): 38 | self.对象.released.connect(回调函数) 39 | 40 | # 绑定事件 按钮选中状态切换 按钮为可选时事件时触发 可选 = True 选中 = True 选中的状态改变时就会触发 41 | def 绑定事件选中状态切换(self, 回调函数): 42 | self.对象.toggled.connect(回调函数) 43 | -------------------------------------------------------------------------------- /go2tv_res/soapcalls/friendlyname_test.go: -------------------------------------------------------------------------------- 1 | package soapcalls 2 | 3 | import ( 4 | "encoding/xml" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestGetFriendlyName(t *testing.T) { 11 | fn := "Just A Friendly Name" 12 | testName := "GetFriendlyName" 13 | type root struct { 14 | FriendlyName string `xml:"device>friendlyName"` 15 | } 16 | 17 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | data := root{ 19 | FriendlyName: fn, 20 | } 21 | 22 | dataXML, _ := xml.Marshal(data) 23 | 24 | w.Header().Set("Content-Type", "text/xml") 25 | if r.Header.Get("Connection") == "close" { 26 | w.Header().Set("Connection", "close") 27 | } 28 | 29 | _, _ = w.Write(dataXML) 30 | })) 31 | 32 | defer testServer.Close() 33 | 34 | friendly, err := GetFriendlyName(testServer.URL) 35 | if err != nil { 36 | t.Fatalf("%s: Failed to call GetFriendlyName due to %s", testName, err.Error()) 37 | } 38 | 39 | if friendly != fn { 40 | t.Fatalf("%s: got: %s, want: %s.", testName, friendly, fn) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /qtefun/组件使用例子/2.复选框组件.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide6.QtWidgets import * 4 | 5 | from qtefun.组件.复选框 import 复选框 6 | 7 | 8 | class Main(QMainWindow): 9 | def __init__(self): 10 | super(Main, self).__init__() 11 | self.init_ui() 12 | 13 | def init_ui(self): 14 | self.resize(400, 400) 15 | self.setWindowTitle("复选框组件学习") 16 | self.show() 17 | 18 | self.复选框 = QCheckBox(self) 19 | self.复选框.move(50, 50) 20 | self.复选框.setText("Hello World!") 21 | self.复选框.show() 22 | 23 | self.复选框1 = 复选框(self.复选框) 24 | self.复选框1.标题 = "祖国您好!" 25 | self.复选框1.选中 = True 26 | self.复选框1.绑定事件选中状态切换(self.选中状态切换) 27 | self.复选框1.绑定事件状态改变(self.绑定事件状态改变) 28 | self.复选框1.绑定事件被点击(self.绑定事件被点击) 29 | 30 | def 选中状态切换(self, 选中状态): 31 | print("选中状态切换", 选中状态) 32 | 33 | def 绑定事件状态改变(self, 状态): 34 | print("绑定事件状态改变", 状态) 35 | 36 | def 绑定事件被点击(self): 37 | print("绑定事件被点击") 38 | 39 | 40 | app = QApplication([]) 41 | # 创建窗口 400x400 42 | win = Main() 43 | 44 | sys.exit(app.exec()) 45 | -------------------------------------------------------------------------------- /qtefun/组件/标签.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | 3 | from qtefun.组件.组件公共类 import 组件公共类 4 | 5 | 6 | class 标签(组件公共类): 7 | 对象 = None # type: QtWidgets.QLabel 8 | 9 | # 获取标题 10 | @property 11 | def 标题(self): 12 | return self.对象.text() 13 | 14 | # 设置标题 15 | @标题.setter 16 | def 标题(self, value: str): 17 | return self.对象.setText(value) 18 | 19 | # 绑定事件 鼠标按下事件 一直按住不松开就会触发 20 | def 绑定事件被按下(self, 回调函数): 21 | """ 22 | 回调函数(e: QMouseEvent) 23 | """ 24 | self.对象.mousePressEvent = 回调函数 25 | 26 | # 绑定事件 鼠标抬起事件 按住按住后松开就会触发 27 | def 绑定事件被松开(self, 回调函数): 28 | """ 29 | 回调函数(e: QMouseEvent) 30 | """ 31 | self.对象.mouseReleaseEvent = 回调函数 32 | 33 | def 绑定事件点击链接(self, 回调函数): 34 | """ 35 | 当用户单击链接时,会发出此信号。锚点引用的URL在/ink中传递。 36 | 37 | 回调函数(链接) 38 | """ 39 | self.对象.linkActivated.connect(回调函数) 40 | def 绑定事件鼠标在链接上停留(self, 回调函数): 41 | """ 42 | 当用户悬挂在链接上时,会发出此信号。锚点引用的URL在链接中传递。 43 | 44 | 回调函数(链接) 45 | """ 46 | self.对象.linkHovered.connect(回调函数) -------------------------------------------------------------------------------- /go2tv_res/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alexandros Ballas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go2tv_res/internal/gui/layouts.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "fyne.io/fyne/v2" 5 | ) 6 | 7 | func (d *mainButtonsLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { 8 | w, h := float32(0), float32(0) 9 | for _, o := range objects { 10 | childSize := o.MinSize() 11 | w += childSize.Width 12 | h = childSize.Height * d.buttonHeight 13 | } 14 | return fyne.NewSize(w, h) 15 | } 16 | 17 | func (d *mainButtonsLayout) Layout(objects []fyne.CanvasObject, containerSize fyne.Size) { 18 | pos := fyne.NewPos(0, 0) 19 | 20 | bigButtonSize := containerSize.Width 21 | for q, o := range objects { 22 | if q != 0 && q != len(objects)-1 { 23 | bigButtonSize = bigButtonSize - o.MinSize().Width 24 | } 25 | } 26 | bigButtonSize = bigButtonSize / 2 27 | 28 | for q, o := range objects { 29 | var size fyne.Size 30 | switch q { 31 | case 0, len(objects) - 1: 32 | size = fyne.NewSize(bigButtonSize, o.MinSize().Height*d.buttonHeight) 33 | default: 34 | size = fyne.NewSize(o.MinSize().Width, o.MinSize().Height*d.buttonHeight) 35 | } 36 | o.Resize(size) 37 | o.Move(pos) 38 | 39 | pos = pos.Add(fyne.NewPos(size.Width, 0)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /qtefun/组件/菜单.py: -------------------------------------------------------------------------------- 1 | import qtawesome 2 | from PySide6.QtGui import QAction 3 | from PySide6.QtWidgets import QMenu 4 | 5 | 6 | class 菜单(QMenu): 7 | 菜单项列表 = {} 8 | parent = None 9 | 10 | def __init__(self, parent=None, 标题=None, 对象名称=None): 11 | super().__init__(parent) 12 | self.parent = parent 13 | if 标题: 14 | self.设置标题(标题) 15 | 16 | if 对象名称: 17 | self.设置对象名称(对象名称) 18 | self.菜单项列表 = {} 19 | 20 | def 设置对象名称(self, 对象名称): 21 | self.setObjectName(对象名称) 22 | 23 | def 设置标题(self, 标题): 24 | self.setTitle(标题) 25 | 26 | def 取菜单项目对象(self, 菜单名): 27 | return self.菜单项列表[菜单名] # type: QAction 28 | 29 | def 添加项目(self, 名字, 图标=None, 回调函数=None,快捷键=None): 30 | 菜单项 = QAction(名字, self.parent) 31 | if 图标: 32 | 菜单项.setIcon(图标) 33 | if 快捷键: 34 | 菜单项.setShortcut(快捷键) 35 | 36 | if 回调函数: 37 | 菜单项.triggered.connect(回调函数) 38 | 39 | self.菜单项列表[名字] = 菜单项 40 | 41 | self.addAction(菜单项) 42 | 43 | def 添加分隔条(self): 44 | self.addSeparator() 45 | 46 | def 取菜单项目(self): 47 | return self.menuAction() 48 | -------------------------------------------------------------------------------- /qtefun/组件/容器.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QTableWidgetItem, QAbstractItemView, QLineEdit, QComboBox, QPushButton, QCheckBox, QWidget 2 | 3 | from qtefun.组件.组件汉化基类 import 组件汉化基类 4 | 5 | 6 | class 容器(组件汉化基类): 7 | 对象 = None # type: # QWidget 8 | 9 | def 设置边框颜色(self, 颜色): 10 | self.对象.setStyleSheet("QWidget{border:1px solid %s}" % 颜色) 11 | 12 | def 设置边框宽度(self, 宽度): 13 | self.对象.setStyleSheet("QWidget{border:%spx solid black}" % 宽度) 14 | 15 | def 设置边框(self, 宽度, 颜色): 16 | self.对象.setStyleSheet("QWidget{border:%spx solid %s}" % (宽度, 颜色)) 17 | 18 | def 设置背景颜色(self, 颜色): 19 | self.对象.setStyleSheet("QWidget{background-color:%s}" % 颜色) 20 | 21 | def 设置背景图片(self, 图片): 22 | self.对象.setStyleSheet("QWidget{background-image:url(%s)}" % 图片) 23 | 24 | def 设置背景图片填充(self, 图片): 25 | self.对象.setStyleSheet( 26 | "QWidget{background-image:url(%s);background-repeat:no-repeat;background-position:center;background-size:contain}" % 图片) 27 | 28 | def 设置背景图片平铺(self, 图片): 29 | self.对象.setStyleSheet( 30 | "QWidget{background-image:url(%s);background-repeat:repeat;background-position:center;background-size:contain}" % 图片) 31 | -------------------------------------------------------------------------------- /qtefun/组件/复选框.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | 3 | from qtefun.组件.组件公共类 import 组件公共类 4 | 5 | # https://doc.qt.io/qt-5/qcheckbox.html 6 | 7 | class 复选框(组件公共类): 8 | 对象 = None # type: QtWidgets.QCheckBox 9 | 10 | # 获取标题 11 | @property 12 | def 标题(self): 13 | return self.对象.text() 14 | 15 | # 设置标题 16 | @标题.setter 17 | def 标题(self, value: str): 18 | return self.对象.setText(value) 19 | 20 | # 设置选中属性 21 | @property 22 | def 选中(self): 23 | return self.对象.isChecked() 24 | 25 | # 设置选中属性 26 | @选中.setter 27 | def 选中(self, value: bool): 28 | return self.对象.setChecked(value) 29 | 30 | # 绑定事件 复选框点击事件 完成一次鼠标点击就会触发 31 | def 绑定事件被点击(self, 回调函数): 32 | self.对象.clicked.connect(回调函数) 33 | 34 | # 绑定事件 复选框选中状态切换 选中 = True 选中的状态改变时就会触发 35 | def 绑定事件选中状态切换(self, 回调函数): 36 | """ 37 | 回调函数(选中状态: bool) 38 | """ 39 | self.对象.toggled.connect(回调函数) 40 | 41 | def 绑定事件状态改变(self, 回调函数): 42 | """ 43 | Qt::Unchecked 0 项目未选中。项目未选中。 44 | Qt::PartiallyChecked 1 项目已部分检查。如果检查了一些(但不是全部)孩子,可以部分检查分层模型中的项目。 45 | Qt::Checked 2 项目已选中。 46 | 47 | 回调函数(状态) 48 | """ 49 | self.对象.stateChanged.connect(回调函数) -------------------------------------------------------------------------------- /go2tv_res/utils/transcode.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package utils 5 | 6 | import ( 7 | "errors" 8 | "io" 9 | "os/exec" 10 | ) 11 | 12 | // ServeTranscodedStream passes an input file or io.Reader to ffmpeg and writes the output directly 13 | // to our io.Writer. 14 | func ServeTranscodedStream(w io.Writer, input interface{}, ff *exec.Cmd) error { 15 | // Pipe streaming is not great as explained here 16 | // https://video.stackexchange.com/questions/34087/ffmpeg-fails-on-pipe-to-pipe-video-decoding. 17 | // That's why if we have the option to pass the file directly to ffmpeg, we should. 18 | var in string 19 | switch f := input.(type) { 20 | case string: 21 | in = f 22 | case io.Reader: 23 | in = "pipe:0" 24 | default: 25 | return errors.New("invalid ffmpeg input") 26 | } 27 | 28 | if ff != nil && ff.Process != nil { 29 | _ = ff.Process.Kill() 30 | } 31 | 32 | cmd := exec.Command( 33 | "ffmpeg", 34 | "-re", 35 | "-i", in, 36 | "-vcodec", "h264", 37 | "-acodec", "aac", 38 | "-ac", "2", 39 | "-vf", "format=yuv420p", 40 | "-movflags", "+faststart", 41 | "-f", "flv", 42 | "pipe:1", 43 | ) 44 | 45 | *ff = *cmd 46 | 47 | if in == "pipe:0" { 48 | ff.Stdin = input.(io.Reader) 49 | } 50 | 51 | ff.Stdout = w 52 | 53 | return ff.Run() 54 | } 55 | -------------------------------------------------------------------------------- /go2tv_res/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alexballas/go2tv 2 | 3 | go 1.16 4 | 5 | require ( 6 | fyne.io/fyne/v2 v2.2.1 7 | github.com/fyne-io/gl-js v0.0.0-20220516203408-b35fbccb7063 // indirect 8 | github.com/fyne-io/glfw-js v0.0.0-20220517201726-bebc2019cd33 // indirect 9 | github.com/gdamore/tcell/v2 v2.5.1 10 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220622232848-a6c407ee30a0 // indirect 11 | github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect 12 | github.com/h2non/filetype v1.1.3 13 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 14 | github.com/hashicorp/go-retryablehttp v0.7.1 15 | github.com/koron/go-ssdp v0.0.3 16 | github.com/mattn/go-runewidth v0.0.13 17 | github.com/pkg/errors v0.9.1 18 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 19 | github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44 // indirect 20 | github.com/srwiley/rasterx v0.0.0-20220615024203-67b7089efd25 // indirect 21 | github.com/yuin/goldmark v1.4.12 // indirect 22 | golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd // indirect 23 | golang.org/x/net v0.0.0-20220614195744-fb05da6f9022 // indirect 24 | golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098 // indirect 25 | golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect 26 | golang.org/x/time v0.0.0-20220609170525-579cf78fd858 27 | ) 28 | -------------------------------------------------------------------------------- /go2tv_res/utils/transcode_windows.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | // ServeTranscodedStream passes an input file or io.Reader to ffmpeg and writes the output directly 11 | // to our io.Writer. 12 | func ServeTranscodedStream(w io.Writer, input interface{}, ff *exec.Cmd) error { 13 | // Pipe streaming is not great as explained here 14 | // https://video.stackexchange.com/questions/34087/ffmpeg-fails-on-pipe-to-pipe-video-decoding. 15 | // That's why if we have the option to pass the file directly to ffmpeg, we should. 16 | var in string 17 | switch f := input.(type) { 18 | case string: 19 | in = f 20 | case io.Reader: 21 | in = "pipe:0" 22 | default: 23 | return errors.New("invalid ffmpeg input") 24 | } 25 | 26 | if ff != nil && ff.Process != nil { 27 | _ = ff.Process.Kill() 28 | } 29 | 30 | cmd := exec.Command( 31 | "ffmpeg", 32 | "-re", 33 | "-i", in, 34 | "-vcodec", "h264", 35 | "-acodec", "aac", 36 | "-ac", "2", 37 | "-vf", "format=yuv420p", 38 | "-movflags", "+faststart", 39 | "-f", "flv", 40 | "pipe:1", 41 | ) 42 | 43 | *ff = *cmd 44 | 45 | // Hide the command window when running ffmpeg. (Windows specific) 46 | cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000} 47 | 48 | if in == "pipe:0" { 49 | ff.Stdin = input.(io.Reader) 50 | } 51 | 52 | ff.Stdout = w 53 | 54 | return ff.Run() 55 | } 56 | -------------------------------------------------------------------------------- /qtefun/组件/组合框.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | from qtefun.组件.组件公共类 import 组件公共类 3 | 4 | 5 | class 组合框(组件公共类): 6 | 对象 = None # type: QtWidgets.QComboBox 7 | # 清空 8 | def 清空(self): 9 | self.对象.clear() 10 | # 添加项目 11 | def 添加项目(self, 文本: str): 12 | return self.对象.addItem(文本) 13 | 14 | # 删除项目 15 | def 删除项目(self, 索引: int): 16 | return self.对象.removeItem(索引) 17 | 18 | # 插入项目 19 | def 插入项目(self, 索引: int, 文本: str): 20 | return self.对象.insertItem(索引, 文本) 21 | 22 | # 取项目数量 23 | def 取项目数量(self): 24 | return self.对象.count() 25 | 26 | # 取项目文本 27 | def 取项目文本(self, 索引: int): 28 | return self.对象.item(索引).text() 29 | 30 | # 取项目索引 31 | def 取项目索引(self, 文本: str): 32 | for i in range(self.对象.count()): 33 | if self.对象.item(i).text() == 文本: 34 | return i 35 | return -1 36 | 37 | # 取选中项目索引 38 | def 取选中项目索引(self): 39 | return self.对象.currentRow() 40 | 41 | # 设置选中项目索引 42 | def 设置选中项目索引(self, 索引: int): 43 | return self.对象.setCurrentRow(索引) 44 | 45 | def 绑定事件项目被选择(self, 回调函数): 46 | """ 47 | 回调函数(索引:int) 48 | """ 49 | self.对象.currentIndexChanged.connect(回调函数) 50 | 51 | # 获取和设置属性 现行选中项 52 | @property 53 | def 现行选中项(self): 54 | return self.取选中项目索引() 55 | 56 | @现行选中项.setter 57 | def 现行选中项(self, 索引: int): 58 | return self.设置选中项目索引(索引) 59 | -------------------------------------------------------------------------------- /qtefun/组件/单行编辑框.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | from PySide6.QtCore import Qt 3 | from PySide6.QtGui import QImage, QColor 4 | 5 | from qtefun.组件.组件公共类 import 组件公共类 6 | 7 | 8 | class 单行编辑框(组件公共类): 9 | """ 10 | 事件 11 | 12 | cursorPositionChanged(int,int) - 当光标位置发生变化时触发 13 | editingFinished() - 当编辑结束时触发 14 | inputRejected() - 当输入被拒绝时触发 15 | returnPressed () - 当用户按下回车键时触发 16 | selectionChanged() - 当选择区域发生变化时触发 17 | textChanged(QString) - 当文本发生变化时触发 18 | textEdited(QString) - 当文本被编辑时触发 19 | 20 | """ 21 | 对象 = None # type: QtWidgets.QLineEdit 22 | 23 | # 获取标题 24 | @property 25 | def 内容(self): 26 | return self.对象.text() 27 | 28 | # 设置内容 29 | @内容.setter 30 | def 内容(self, value: str): 31 | print("设置标题", value) 32 | return self.对象.setText(value) 33 | 34 | def 绑定事件内容被改变(self, 回调函数): 35 | """ 36 | 当文本被编辑时触发 37 | :param 回调函数: 回调函数(文本) 38 | """ 39 | return self.对象.textChanged.connect(回调函数) 40 | 41 | def 绑定事件编辑完成(self, 回调函数): 42 | return self.对象.editingFinished.connect(回调函数) 43 | 44 | def 绑定事件输入被拒绝(self, 回调函数): 45 | return self.对象.inputRejected.connect(回调函数) 46 | 47 | def 绑定事件回车键被按下(self, 回调函数): 48 | return self.对象.returnPressed.connect(回调函数) 49 | 50 | def 绑定事件选择区域发生变化(self, 回调函数): 51 | return self.对象.selectionChanged.connect(回调函数) 52 | 53 | def 绑定事件文本被编辑(self, 回调函数): 54 | """ 55 | 当文本被编辑时触发 56 | :param 回调函数: 回调函数(文本) 57 | """ 58 | return self.对象.textEdited.connect(回调函数) 59 | -------------------------------------------------------------------------------- /go2tv_res/soapcalls/xmlparsers_test.go: -------------------------------------------------------------------------------- 1 | package soapcalls 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestDMRextractor(t *testing.T) { 10 | raw := ` 11 | 12 | 13 | 14 | 15 | urn:schemas-upnp-org:service:RenderingControl:1 16 | urn:upnp-org:serviceId:RenderingControl 17 | /upnp/control/RenderingControl1 18 | /upnp/event/RenderingControl1 19 | /RenderingControl_1.xml 20 | 21 | 22 | urn:schemas-upnp-org:service:ConnectionManager:1 23 | urn:upnp-org:serviceId:ConnectionManager 24 | /upnp/control/ConnectionManager1 25 | /upnp/event/ConnectionManager1 26 | /ConnectionManager_1.xml 27 | 28 | 29 | urn:schemas-upnp-org:service:AVTransport:1 30 | urn:upnp-org:serviceId:AVTransport 31 | /upnp/control/AVTransport1 32 | /upnp/event/AVTransport1 33 | /AVTransport_1.xml 34 | 35 | 36 | 37 | ` 38 | 39 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 | _, _ = w.Write([]byte(raw)) 41 | })) 42 | 43 | defer testServer.Close() 44 | 45 | _, err := DMRextractor(testServer.URL) 46 | if err != nil { 47 | t.Fatalf("Failed to call DMRextractor due to %s", err.Error()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /qtefun/组件/工具条.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from PySide6.QtCore import Qt 5 | from PySide6.QtGui import QIcon, QAction 6 | from PySide6.QtWidgets import QMenu, QMenuBar, QToolBar, QToolButton 7 | import pyefun as efun 8 | 9 | from qtefun.组件.组件公共类 import 组件公共类 10 | 11 | 12 | class 工具条(组件公共类): 13 | parent = None 14 | 对象 = None # type: QToolButton 15 | 菜单项列表={} 16 | 资源文件绝对路径 = "" 17 | def 添加分隔条(self): 18 | self.addSeparator() 19 | 20 | def 添加项目(self, 名字, 图标, 帮助文本, 图标宽度=32, 图标高度=32, 回调函数=None): 21 | 菜单项 = QAction(名字, self.parent) 22 | if 图标: 23 | 菜单项.setIcon(图标) 24 | # if 快捷键: 25 | # 菜单项.setShortcut(快捷键) 26 | 菜单项.setToolTip(帮助文本) 27 | if 回调函数: 28 | 菜单项.triggered.connect(回调函数) 29 | self.菜单项列表[名字] = 菜单项 30 | self.addAction(菜单项) 31 | 32 | def 从工具条数据中创建(self, 工具条数据, 图标宽度=32, 图标高度=32, 回调函数=None): 33 | toolJson = json.loads(工具条数据) 34 | for 第一层的值 in toolJson: 35 | # id = 第一层的值.get("id") 36 | 名称 = 第一层的值.get("名称") 37 | 图标 = 第一层的值.get("图标") 38 | 帮助文本 = 第一层的值.get("帮助文本") 39 | if 名称 == "-": 40 | self.添加分隔条() 41 | continue 42 | 43 | if 帮助文本 is None: 44 | 帮助文本 = 名称 45 | 46 | if 图标 is not None: 47 | 图标 = efun.子文本替换(图标, "./", self.资源文件绝对路径 + "/") 48 | 图标 = efun.路径优化(图标) 49 | if efun.文件是否存在(图标): 50 | image = QIcon(图标) 51 | self.添加项目(名称, image, 帮助文本, 图标宽度, 图标高度, 回调函数) 52 | else: 53 | print("工具条图标文件不存在无法创建[{}]文件路径[{}]".format(名称, 图标, )) 54 | -------------------------------------------------------------------------------- /qtefun/组件使用例子/6.表格.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide6.QtWidgets import * 4 | 5 | from qtefun.组件.表格 import 表格 6 | 7 | 8 | class Main(QMainWindow): 9 | def __init__(self): 10 | super(Main, self).__init__() 11 | self.init_ui() 12 | 13 | def init_ui(self): 14 | self.resize(600, 600) 15 | self.setWindowTitle("表格组件学习") 16 | self.show() 17 | 18 | self.表格 = QTableWidget(self) 19 | self.表格1 = 表格(self.表格) 20 | self.表格.setGeometry(0, 0, 600, 400) 21 | self.表格.show() 22 | self.表格1.设置列数(4) 23 | # self.表格1.设置行数(100) 24 | self.表格1.设置表头(['分类', "产品名称", '价格', '说明']) 25 | self.表格1.设置整行选择() 26 | self.表格1.设置单一选择() 27 | 28 | # 插入10行测试数据 29 | for i in range(5): 30 | self.表格1.插入行(i) 31 | self.表格1.设置项目文本(i, 0, "分类1") 32 | self.表格1.设置项目文本(i, 1, "产品名称1") 33 | self.表格1.设置项目文本(i, 2, "价格1") 34 | self.表格1.设置项目文本(i, 3, "说明1") 35 | 36 | for i in range(5): 37 | self.表格1.插入行(i) 38 | self.表格1.设置单元格按钮(i, 0, "操作", lambda 行号, 列号: print(行号, 列号)) 39 | self.表格1.设置单元格复选框(i, 1, True, "选择", lambda 行号, 列号, 状态: print(行号, 列号, 状态)) 40 | self.表格1.设置单元格文本框(i, 2, "价格1", lambda 行号, 列号, 文本: print(行号, 列号, 文本)) 41 | self.表格1.设置单元格组合框(i, 3, ['真', '假'], 0, lambda 行号, 列号, 文本: print(行号, 列号, 文本)) 42 | 43 | # 创建按钮 44 | self.按钮 = QPushButton(self) 45 | self.按钮.clicked.connect(self.按钮点击) 46 | self.按钮.move(0, 400) 47 | self.按钮.resize(100, 40) 48 | self.按钮.setText('查找') 49 | self.按钮.show() 50 | 51 | def 按钮点击(self): 52 | pass 53 | 数据 = self.表格1.导出数据() 54 | print(数据) 55 | 56 | 57 | 58 | app = QApplication([]) 59 | # 创建窗口 400x400 60 | win = Main() 61 | 62 | sys.exit(app.exec()) 63 | -------------------------------------------------------------------------------- /go2tv_res/devices/devices.go: -------------------------------------------------------------------------------- 1 | package devices 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/alexballas/go2tv/soapcalls" 8 | "github.com/koron/go-ssdp" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | var ErrNoDeviceAvailable = errors.New("loadSSDPservices: No available Media Renderers") 13 | 14 | // LoadSSDPservices returns a map with all the devices that support the 15 | // AVTransport service. 16 | func LoadSSDPservices(delay int) (map[string]string, error) { 17 | // Reset device list every time we call this. 18 | deviceList := make(map[string]string) 19 | list, err := ssdp.Search(ssdp.All, delay, "") 20 | if err != nil { 21 | return nil, fmt.Errorf("LoadSSDPservices search error: %w", err) 22 | } 23 | 24 | for _, srv := range list { 25 | // We only care about the AVTransport services for basic actions 26 | // (stop,play,pause). If we need support other functionalities 27 | // like volume control we need to use the RenderingControl service. 28 | if srv.Type == "urn:schemas-upnp-org:service:AVTransport:1" { 29 | friendlyName, err := soapcalls.GetFriendlyName(srv.Location) 30 | if err != nil { 31 | continue 32 | } 33 | 34 | deviceList[friendlyName] = srv.Location 35 | } 36 | } 37 | 38 | if len(deviceList) > 0 { 39 | return deviceList, nil 40 | } 41 | 42 | return nil, ErrNoDeviceAvailable 43 | } 44 | 45 | // DevicePicker will pick the nth device from the devices input map. 46 | func DevicePicker(devices map[string]string, n int) (string, error) { 47 | if n > len(devices) || len(devices) == 0 || n <= 0 { 48 | return "", errors.New("devicePicker: Requested device not available") 49 | } 50 | 51 | keys := make([]string, 0) 52 | for k := range devices { 53 | keys = append(keys, k) 54 | } 55 | 56 | sort.Strings(keys) 57 | 58 | for q, k := range keys { 59 | if n == q+1 { 60 | return devices[k], nil 61 | } 62 | } 63 | return "", errors.New("devicePicker: Something went terribly wrong") 64 | } 65 | -------------------------------------------------------------------------------- /qtefun/组件/纯文本编辑框.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | from PySide6.QtCore import Qt 3 | from PySide6.QtGui import QImage, QColor 4 | 5 | from qtefun.组件.组件公共类 import 组件公共类 6 | 7 | 8 | class 纯文本编辑框(组件公共类): 9 | """ 10 | 事件 11 | 12 | def blockCountChanged (newBlockCount) - 当文本块数发生变化时触发 13 | 14 | def copyAvailable (b) - 当前文本被拷贝到剪贴板时触发 15 | 16 | def cursorPositionChanged () - 光标位置已更改 17 | 18 | def modificationChanged (arg__1) - 修改已更改 19 | 20 | def redoAvailable (b) - 当前文本可以重做时触发 21 | 22 | def selectionChanged () - 选择已更改 23 | 24 | def textChanged () - 文本已更改 25 | 26 | def undoAvailable (b) - 当前文本可以撤销时触发 27 | 28 | def updateRequest (rect, dy) - 更新请求 29 | 30 | """ 31 | 对象 = None # type: QtWidgets.QPlainTextEdit 32 | 33 | # 获取标题 34 | @property 35 | def 内容(self): 36 | return self.对象.toPlainText() 37 | 38 | # 设置内容 39 | @内容.setter 40 | def 内容(self, value: str): 41 | print("设置标题", value) 42 | return self.对象.setPlainText(value) 43 | 44 | def 绑定事件内容被改变(self, 回调函数): 45 | """ 46 | 回调函数(是否可撤销:bool) 47 | """ 48 | return self.对象.textChanged.connect(回调函数) 49 | 50 | 51 | def 绑定事件块数量改变(self,回调函数): 52 | return self.对象.blockCountChanged.connect(回调函数) 53 | 54 | def 绑定事件光标位置被改变(self,回调函数): 55 | return self.对象.cursorPositionChanged.connect(回调函数) 56 | 57 | def 绑定事件被修改(self,回调函数): 58 | return self.对象.modificationChanged.connect(回调函数) 59 | 60 | def 绑定事件文本可复制(self,回调函数): 61 | return self.对象.copyAvailable.connect(回调函数) 62 | 63 | def 绑定事件文本可重做(self,回调函数): 64 | return self.对象.redoAvailable.connect(回调函数) 65 | 66 | def 绑定事件文本可撤销(self,回调函数): 67 | return self.对象.undoAvailable.connect(回调函数) 68 | 69 | def 绑定事件选择文本(self,回调函数): 70 | return self.对象.selectionChanged.connect(回调函数) 71 | 72 | def 绑定事件更新请求(self,回调函数): 73 | return self.对象.updateRequest.connect(回调函数) 74 | -------------------------------------------------------------------------------- /qtefun/组件/富文本编辑框.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | from PySide6.QtCore import Qt 3 | from PySide6.QtGui import QImage, QColor 4 | 5 | from qtefun.组件.组件公共类 import 组件公共类 6 | 7 | 8 | class 富文本编辑框(组件公共类): 9 | """ 10 | 事件 11 | 12 | copyAvailable(bool) - 当前文本被拷贝到剪贴板时触发 13 | currentCharFormatChanged(QTextCharFormat) - 当前文本格式发生变化时触发 内容被改变 14 | cursorPositionChanged () - 当前光标位置发生变化时触发 15 | redoAvailable(bool) - 当前文本可以重做时触发 16 | selectionChanged() - 当前文本选择发生变化时触发 17 | textChanged () - 当前文本发生变化时触发 18 | undoAvailable(bool) - 当前文本可以撤销时触发 19 | 20 | """ 21 | 对象 = None # type: QtWidgets.QTextEdit 22 | 23 | # 获取标题 24 | @property 25 | def 内容(self): 26 | return self.对象.toPlainText() 27 | 28 | # 设置内容 29 | @内容.setter 30 | def 内容(self, value: str): 31 | print("设置标题", value) 32 | return self.对象.setText(value) 33 | 34 | def 绑定事件内容被改变(self, 回调函数): 35 | return self.对象.textChanged.connect(回调函数) 36 | 37 | def 绑定事件文本可复制(self, 回调函数): 38 | """ 39 | 回调函数(是否选择文本:bool) 40 | """ 41 | # 该信号在文本编辑中选择或取消选择文本时发出。 42 | # 当选择文本时,将发出该信号,并将yes设置为true。如果未选择任何文本或取消选择所选文本,则发出该信号,并将yes设置为false。 43 | # 如果yes为true,则可以使用copy()将所选内容复制到剪贴板。如果yes为false,则copy()不执行任何操作。 44 | return self.对象.copyAvailable.connect(回调函数) 45 | 46 | def 绑定事件字符格式更改(self, 回调函数): 47 | # 如果当前字符格式已更改,例如由于光标位置的更改而导致,则会发出该信号。 48 | return self.对象.currentCharFormatChanged.connect(回调函数) 49 | 50 | def 绑定事件光标位置被改变(self, 回调函数): 51 | return self.对象.cursorPositionChanged.connect(回调函数) 52 | 53 | def 绑定事件文本可重做(self, 回调函数): 54 | # 每当重做操作可用(可用为true)或不可用(可用为false)时,就会发出此信号。 55 | return self.对象.redoAvailable.connect(回调函数) 56 | 57 | def 绑定事件选择文本(self, 回调函数): 58 | # 每当选择发生变化时,都会发出此信号。 59 | return self.对象.selectionChanged.connect(回调函数) 60 | 61 | def 绑定事件文本可撤销(self, 回调函数): 62 | """ 63 | 回调函数(是否可撤销:bool) 64 | """ 65 | return self.对象.undoAvailable.connect(回调函数) 66 | -------------------------------------------------------------------------------- /go2tv_res/assets/go2tv-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /投屏模块.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import sys 4 | from nanodlna import dlna, devices 5 | 6 | import 文件服务类 7 | from pyefun import * 8 | 9 | 10 | def 获取设备列表(): 11 | 设备列表 = [] 12 | my_devices = devices.get_devices(3) 13 | for i, device in enumerate(my_devices, 1): 14 | 设备列表.append({ 15 | "Model": device["friendly_name"], 16 | "URL": device["location"], 17 | }) 18 | return 设备列表 19 | 20 | 21 | def 取局域网ip(target_ip, target_port=80): 22 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 23 | s.connect((target_ip, target_port)) 24 | serve_ip = s.getsockname()[0] 25 | s.close() 26 | return serve_ip 27 | 28 | 29 | def 正则取中文数字字母(欲处理文本: str) -> str: 30 | import re 31 | 正则表达式 = re.compile(r'[^\u4e00-\u9fa5a-zA-Z0-9\.]') 32 | return 正则表达式.sub('', 欲处理文本) 33 | 34 | 35 | def 投递视频文件(设备url, 文件路径): 36 | device = devices.register_device(设备url) 37 | print("device", device) 38 | if not device: 39 | sys.exit("No devices found.") 40 | target_ip = device["hostname"] 41 | 局域网ip = 取局域网ip(target_ip) 42 | if 文件路径.startswith("http"): 43 | # 对文件路径url编码 44 | 播放地址 = 文件路径 45 | else: 46 | 文件名 = os.path.basename(文件路径) 47 | # 取扩展名 48 | 扩展名 = os.path.splitext(文件名)[1] 49 | 文件名 = 取短id() + "." + 扩展名 50 | 51 | # 文件名 = 正则取中文数字字母(文件名) 52 | # 文件名 = 编码_URL编码(文件名) 53 | 54 | 文件服务类.写文件名与路径(文件名, 文件路径) 55 | 播放地址 = f"http://{局域网ip}:6161/{文件名}" 56 | files_urls = {'file_video': 播放地址} 57 | print("Files URLs: {}".format(files_urls)) 58 | dlna.play(files_urls, device) 59 | return device, 播放地址 60 | 61 | 62 | def 暂停播放(device): 63 | dlna.pause(device) 64 | 65 | 66 | def 停止播放(device): 67 | dlna.stop(device) 68 | 69 | 70 | if __name__ == '__main__': 71 | pass 72 | print(获取设备列表()) 73 | # device = 投递视频文件("http://192.168.31.239:57873/description.xml", 74 | # "/Users/chensuilong/Documents/lzxd/廉政行动2022.2022.EP01.HD1080P.X264.AAC.Cantonese.CHS.BDYS.mp4") 75 | # sleep(5) 76 | # 停止播放(device) 77 | -------------------------------------------------------------------------------- /go2tv_res/utils/iptools.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // URLtoListenIPandPort for a given internal URL, 13 | // find the correct IP/Interface to listen to. 14 | func URLtoListenIPandPort(u string) (string, error) { 15 | parsedURL, err := url.Parse(u) 16 | if err != nil { 17 | return "", fmt.Errorf("URLtoListenIPandPort parse error: %w", err) 18 | } 19 | 20 | callURL := parsedURL.Host 21 | if parsedURL.Port() == "" { 22 | switch parsedURL.Scheme { 23 | case "http": 24 | callURL = callURL + ":80" 25 | case "https": 26 | callURL = callURL + ":443" 27 | } 28 | } 29 | 30 | conn, err := net.Dial("udp", callURL) 31 | if err != nil { 32 | return "", fmt.Errorf("URLtoListenIPandPort UDP call error: %w", err) 33 | } 34 | 35 | ipToListen := strings.Split(conn.LocalAddr().String(), ":")[0] 36 | portToListen, err := checkAndPickPort(ipToListen, 3500) 37 | if err != nil { 38 | return "", fmt.Errorf("URLtoListenIPandPort port error: %w", err) 39 | } 40 | 41 | res := net.JoinHostPort(ipToListen, portToListen) 42 | 43 | return res, nil 44 | } 45 | 46 | func checkAndPickPort(ip string, port int) (string, error) { 47 | var numberOfchecks int 48 | CHECK: 49 | numberOfchecks++ 50 | conn, err := net.Listen("tcp", net.JoinHostPort(ip, strconv.Itoa(port))) 51 | if err != nil { 52 | if strings.Contains(err.Error(), "address already in use") { 53 | if numberOfchecks == 1000 { 54 | return "", fmt.Errorf("port pick error. Checked 1000 ports: %w", err) 55 | } 56 | port++ 57 | goto CHECK 58 | } 59 | 60 | return "", fmt.Errorf("port pick error: %w", err) 61 | } 62 | conn.Close() 63 | return strconv.Itoa(port), nil 64 | } 65 | 66 | // HostPortIsAlive - We use this function to periodically 67 | // health check the selected device and decide if we want 68 | // to keep the entry in the list or to remove it. 69 | func HostPortIsAlive(h string) bool { 70 | conn, err := net.DialTimeout("tcp", h, time.Duration(2*time.Second)) 71 | if err != nil { 72 | return false 73 | } 74 | conn.Close() 75 | return true 76 | } 77 | -------------------------------------------------------------------------------- /go2tv模块.py: -------------------------------------------------------------------------------- 1 | from pyefun import * 2 | from pyefun.模块.终端类 import * 3 | 4 | import re 5 | 6 | if 是否为PyInstaller编译后环境(): 7 | 全局变量_资源文件目录 = 取资源文件路径() 8 | else: 9 | 全局变量_资源文件目录 = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | def 获取设备列表(): 13 | # 内容 = """ 14 | # Device 1 15 | # -------- 16 | # Model: 华为智慧屏 S65 17 | # URL: http://192.168.31.239:57873/description.xml 18 | # 19 | # Device 2 20 | # -------- 21 | # Model: 小爱音箱-3224 22 | # URL: http://192.168.31.52:9999/bab5411b-9c23-4c6a-827d-4b463943da0b.xml 23 | # """ 24 | 内容 = "" 25 | if 系统_是否为window系统(): 26 | 命令 = f"{全局变量_资源文件目录}/go2tv/go2tv.exe -l" 27 | print(命令) 28 | 终端 = 终端类() 29 | 终端.运行(命令) 30 | 内容 = 终端.取返回结果() 31 | 32 | if 系统_是否为mac系统(): 33 | 命令 = f"{全局变量_资源文件目录}/go2tv/go2tv -l" 34 | print(命令) 35 | 终端 = 终端类() 36 | 终端.运行(命令) 37 | 内容 = 终端.取返回结果() 38 | # 正则获取内容中的 Model URL 39 | 正则 = re.compile(r'Model: (.*?)\nURL: (.*?)\n') 40 | 设备列表 = 正则.findall(内容) 41 | # 删除 42 | 设备列表 = [{'Model': x[0], 'URL': x[1]} for x in 设备列表] 43 | # 删除空白字符 44 | 设备列表 = [{'Model': x['Model'].strip(), 'URL': x['URL'].strip()} for x in 设备列表] 45 | return 设备列表 46 | 47 | 48 | def 投递视频文件(设备url, 文件路径): 49 | # ./go2tv -v /Users/chensuilong/Documents/lzxd/廉政行动2022.2022.EP01.HD1080P.X264.AAC.Cantonese.CHS.BDYS.mp4 -t http://192.168.31.10:25826/description.xml 50 | 命令 = 取运行目录() + f"/go2tv -v {文件路径} -t {设备url}" 51 | 运行(命令) 52 | # 终端 = 终端类() 53 | # 终端.运行(命令) 54 | # print(命令) 55 | # from applescript import tell 56 | # tell.app('Terminal', 'do script "' + 命令 + '"') 57 | 58 | 59 | def 结束http服务器(): 60 | 运行("killall go2tv") 61 | 62 | 63 | if __name__ == "__main__": 64 | pass 65 | 设备列表 = 获取设备列表() 66 | for x in 设备列表: 67 | Model, URL = x['Model'], x['URL'] 68 | print(Model, URL) 69 | 70 | # 设备URL = "http://192.168.31.239:57873/description.xml" 71 | # 播放文件路径 = "/Users/chensuilong/Documents/lzxd/廉政行动2022.2022.EP01.HD1080P.X264.AAC.Cantonese.CHS.BDYS.mp4" 72 | # 投递视频文件(设备URL, 播放文件路径) 73 | # 线程 = 启动线程(工作线程, 跟随主线程结束=True) 74 | -------------------------------------------------------------------------------- /go2tv_res/internal/gui/settings.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "image/color" 5 | 6 | "fyne.io/fyne/v2" 7 | "fyne.io/fyne/v2/canvas" 8 | "fyne.io/fyne/v2/container" 9 | "fyne.io/fyne/v2/theme" 10 | "fyne.io/fyne/v2/widget" 11 | ) 12 | 13 | type go2tvTheme struct { 14 | Theme string 15 | } 16 | 17 | func (m go2tvTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { 18 | switch m.Theme { 19 | case "Dark": 20 | variant = theme.VariantDark 21 | case "Light": 22 | variant = theme.VariantLight 23 | } 24 | 25 | return theme.DefaultTheme().Color(name, variant) 26 | } 27 | 28 | func (m go2tvTheme) Icon(name fyne.ThemeIconName) fyne.Resource { 29 | return theme.DefaultTheme().Icon(name) 30 | } 31 | 32 | func (m go2tvTheme) Font(style fyne.TextStyle) fyne.Resource { 33 | return theme.DefaultTheme().Font(style) 34 | } 35 | 36 | func (m go2tvTheme) Size(name fyne.ThemeSizeName) float32 { 37 | return theme.DefaultTheme().Size(name) 38 | } 39 | 40 | func settingsWindow(s *NewScreen) fyne.CanvasObject { 41 | themeText := canvas.NewText("Theme", nil) 42 | dropdown := widget.NewSelect([]string{"Light", "Dark", "Default"}, parseTheme(s)) 43 | theme := fyne.CurrentApp().Preferences().StringWithFallback("Theme", "Default") 44 | switch theme { 45 | case "Light": 46 | dropdown.PlaceHolder = "Light" 47 | case "Dark": 48 | dropdown.PlaceHolder = "Dark" 49 | case "Default": 50 | dropdown.PlaceHolder = "Default" 51 | } 52 | 53 | dropdown.Refresh() 54 | 55 | settings := container.NewVBox(themeText, dropdown) 56 | return settings 57 | } 58 | 59 | func parseTheme(s *NewScreen) func(string) { 60 | return func(t string) { 61 | switch t { 62 | case "Light": 63 | fyne.CurrentApp().Preferences().SetString("Theme", "Light") 64 | fyne.CurrentApp().Settings().SetTheme(go2tvTheme{"Light"}) 65 | case "Dark": 66 | fyne.CurrentApp().Preferences().SetString("Theme", "Dark") 67 | fyne.CurrentApp().Settings().SetTheme(go2tvTheme{"Dark"}) 68 | case "Default": 69 | fyne.CurrentApp().Preferences().SetString("Theme", "Default") 70 | fyne.CurrentApp().Settings().SetTheme(go2tvTheme{"Default"}) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /go2tv_res/utils/iptools_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestURLtoListenIPandPort(t *testing.T) { 11 | tt := []struct { 12 | name string 13 | input string 14 | wantFromPort int 15 | wantToPort int 16 | }{ 17 | { 18 | `Test #1`, 19 | `http://192.168.88.244:9197/dmr`, 20 | 3500, 21 | 4500, 22 | }, 23 | { 24 | `Test #2`, 25 | `http://192.168.2.211/dmr`, 26 | 3500, 27 | 4500, 28 | }, 29 | { 30 | `Test #3`, 31 | `https://192.168.1.2/dmr`, 32 | 3500, 33 | 4500, 34 | }, 35 | } 36 | 37 | for _, tc := range tt { 38 | t.Run(tc.name, func(t *testing.T) { 39 | out, err := URLtoListenIPandPort(tc.input) 40 | if err != nil { 41 | t.Fatalf("%s: Failed to call URLtoListenIPandPort due to %s", tc.name, err.Error()) 42 | } 43 | outSplit := strings.Split(out, ":") 44 | 45 | if len(outSplit) < 2 { 46 | t.Fatalf("%s: Not in ip:port format: %s", tc.name, err.Error()) 47 | } 48 | 49 | outInt, _ := strconv.Atoi(outSplit[1]) 50 | 51 | if outInt < tc.wantFromPort || outInt > tc.wantToPort { 52 | t.Fatalf("%s: got: %s, wanted port between: %d - %d.", tc.name, out, tc.wantFromPort, tc.wantToPort) 53 | } 54 | }) 55 | } 56 | } 57 | func TestCheckAndPickPort(t *testing.T) { 58 | tt := []struct { 59 | name string 60 | inputHost string 61 | inputPort int 62 | }{ 63 | { 64 | `Test #1`, 65 | "127.0.0.1", 66 | 3000, 67 | }, 68 | } 69 | 70 | for _, tc := range tt { 71 | t.Run(tc.name, func(t *testing.T) { 72 | _, err := checkAndPickPort(tc.inputHost, tc.inputPort) 73 | if err != nil { 74 | t.Fatalf("%s: Failed to call TestCheckAndPickPort due to %s", tc.name, err.Error()) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | func TestHostPortIsAlive(t *testing.T) { 81 | ln, err := net.Listen("tcp", "127.0.0.1:0") 82 | if err != nil { 83 | t.Fatalf("HostPortIsAlive: failed to start server") 84 | } 85 | go func() { 86 | defer ln.Close() 87 | _, _ = ln.Accept() 88 | }() 89 | 90 | if !HostPortIsAlive(ln.Addr().String()) { 91 | t.Fatalf("HostPortIsAlive: expected true") 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /qtefun/组件使用例子/7.容器.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide6.QtCore import QRect, QCoreApplication 4 | from PySide6.QtWidgets import * 5 | 6 | from qtefun.组件.容器 import * 7 | from qtefun.组件.表格 import 表格 8 | 9 | 10 | class Main(QMainWindow): 11 | def __init__(self): 12 | super(Main, self).__init__() 13 | self.init_ui() 14 | 15 | def init_ui(self): 16 | self.resize(600, 600) 17 | self.setWindowTitle("容器组件学习") 18 | self.show() 19 | 20 | self.centralwidget = QWidget(self) 21 | self.centralwidget.setObjectName(u"centralwidget") 22 | self.widget = QWidget(self.centralwidget) 23 | self.容器 = 容器(self.widget) 24 | self.容器.设置背景颜色("#ffffff") 25 | 26 | self.widget.setObjectName(u"widget") 27 | self.widget.setGeometry(QRect(10, 20, 401, 101)) 28 | print(self.widget) 29 | self.radioButton_2 = QRadioButton(self.widget) 30 | self.radioButton_2.setObjectName(u"radioButton_2") 31 | self.radioButton_2.setGeometry(QRect(160, 20, 99, 20)) 32 | self.radioButton = QRadioButton(self.widget) 33 | self.radioButton.setObjectName(u"radioButton") 34 | self.radioButton.setGeometry(QRect(30, 20, 99, 20)) 35 | self.widget_2 = QWidget(self.centralwidget) 36 | self.widget_2.setObjectName(u"widget_2") 37 | self.widget_2.setGeometry(QRect(10, 130, 371, 80)) 38 | self.radioButton_3 = QRadioButton(self.widget_2) 39 | self.radioButton_3.setObjectName(u"radioButton_3") 40 | self.radioButton_3.setGeometry(QRect(40, 20, 99, 20)) 41 | self.radioButton_4 = QRadioButton(self.widget_2) 42 | self.radioButton_4.setObjectName(u"radioButton_4") 43 | self.radioButton_4.setGeometry(QRect(200, 20, 99, 20)) 44 | 45 | self.setCentralWidget(self.centralwidget) 46 | 47 | self.radioButton_2.setText(QCoreApplication.translate("MainWindow", u"RadioButton", None)) 48 | self.radioButton.setText(QCoreApplication.translate("MainWindow", u"RadioButton", None)) 49 | self.radioButton_3.setText(QCoreApplication.translate("MainWindow", u"RadioButton", None)) 50 | self.radioButton_4.setText(QCoreApplication.translate("MainWindow", u"RadioButton", None)) 51 | app = QApplication([]) 52 | # 创建窗口 400x400 53 | win = Main() 54 | 55 | sys.exit(app.exec()) 56 | -------------------------------------------------------------------------------- /go2tv_res/internal/interactive/interactive.go: -------------------------------------------------------------------------------- 1 | package interactive 2 | 3 | import ( 4 | "fmt" 5 | "github.com/alexballas/go2tv/soapcalls" 6 | "github.com/gdamore/tcell/v2" 7 | "os" 8 | "strconv" 9 | "sync" 10 | ) 11 | 12 | // NewScreen . 13 | type NewScreen struct { 14 | mu sync.RWMutex 15 | TV *soapcalls.TVPayload 16 | mediaTitle string 17 | lastAction string 18 | } 19 | 20 | var flipflop bool = true 21 | 22 | // InterInit starts the interactive terminal 23 | func (p *NewScreen) InterInit(tv *soapcalls.TVPayload) { 24 | p.TV = tv 25 | //由于我们需要正确初始化 tcell 窗口,因此更早发送 Play1 操作可能会导致恐慌错误。 26 | if err := tv.SendtoTV("Play1"); err != nil { 27 | fmt.Fprintf(os.Stderr, "%v\n", err) 28 | os.Exit(1) 29 | } 30 | 服务器启动 := make(chan struct{}) 31 | <-服务器启动 32 | 33 | } 34 | 35 | // HandleKeyEvent Method to handle all key press events 36 | func (p *NewScreen) HandleKeyEvent(ev *tcell.EventKey) { 37 | tv := p.TV 38 | 39 | if ev.Key() == tcell.KeyEscape { 40 | tv.SendtoTV("Stop") 41 | p.Fini() 42 | } 43 | 44 | if ev.Key() == tcell.KeyPgUp || ev.Key() == tcell.KeyPgDn { 45 | currentVolume, err := tv.GetVolumeSoapCall() 46 | if err != nil { 47 | return 48 | } 49 | 50 | setVolume := currentVolume - 1 51 | if ev.Key() == tcell.KeyPgUp { 52 | setVolume = currentVolume + 1 53 | } 54 | 55 | stringVolume := strconv.Itoa(setVolume) 56 | 57 | if err := tv.SetVolumeSoapCall(stringVolume); err != nil { 58 | return 59 | } 60 | } 61 | 62 | switch ev.Rune() { 63 | case 'p': 64 | if flipflop { 65 | flipflop = false 66 | tv.SendtoTV("Pause") 67 | break 68 | } 69 | 70 | flipflop = true 71 | tv.SendtoTV("Play") 72 | 73 | case 'm': 74 | currentMute, err := tv.GetMuteSoapCall() 75 | if err != nil { 76 | break 77 | } 78 | switch currentMute { 79 | case "1": 80 | if err = tv.SetMuteSoapCall("0"); err == nil { 81 | } 82 | case "0": 83 | if err = tv.SetMuteSoapCall("1"); err == nil { 84 | } 85 | } 86 | } 87 | } 88 | 89 | // Fini Method to implement the screen interface 90 | func (p *NewScreen) Fini() { 91 | os.Exit(0) 92 | } 93 | 94 | // InitTcellNewScreen . 95 | func InitTcellNewScreen() (*NewScreen, error) { 96 | return &NewScreen{}, nil 97 | } 98 | 99 | func (p *NewScreen) getLastAction() string { 100 | p.mu.RLock() 101 | defer p.mu.RUnlock() 102 | return p.lastAction 103 | } 104 | 105 | func (p *NewScreen) updateLastAction(s string) { 106 | p.mu.Lock() 107 | defer p.mu.Unlock() 108 | p.lastAction = s 109 | } 110 | -------------------------------------------------------------------------------- /qtefun/组件使用例子/4.列表框.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide6.QtWidgets import * 4 | 5 | from qtefun.组件.列表框 import 列表框 6 | 7 | 8 | class Main(QMainWindow): 9 | def __init__(self): 10 | super(Main, self).__init__() 11 | self.init_ui() 12 | 13 | def init_ui(self): 14 | self.resize(400, 400) 15 | self.setWindowTitle("列表框组件学习") 16 | self.show() 17 | 18 | self.列表框 = QListWidget(self) 19 | self.列表框.setGeometry(50, 50, 200, 200) 20 | self.列表框.show() 21 | 22 | self.列表框1 = 列表框(self.列表框) 23 | 24 | self.列表框1.添加项目("祖国您好!1") 25 | self.列表框1.添加项目("祖国您好!2") 26 | self.列表框1.添加项目("祖国您好!3") 27 | self.列表框1.添加项目("祖国您好!4") 28 | self.列表框1.添加项目("祖国您好!5") 29 | 30 | self.列表框1.删除项目(4) 31 | self.列表框1.删除项目(0) 32 | 33 | # 插入项目 34 | self.列表框1.插入项目(0, "祖国您好!6") 35 | 36 | 项目数量 = self.列表框1.取项目数量() 37 | print("项目数量", 项目数量) 38 | 39 | 项目文本 = self.列表框1.取项目文本(0) 40 | print("项目文本", 项目文本) 41 | 42 | # 取项目索引 43 | 项目索引 = self.列表框1.取项目索引("祖国您好!3") 44 | print("项目索引", 项目索引) 45 | 46 | # 取当前选中项目索引 47 | 选中项目索引 = self.列表框1.取选中项目索引() 48 | print("选中项目索引", 选中项目索引) 49 | 50 | # 绑定事件选中项目被改变 51 | self.列表框1.绑定事件选中项目已更改(self.项目选择已更改) 52 | # 绑定事件项目被点击 53 | self.列表框1.绑定事件项目被点击(self.项目被点击) 54 | # 绑定事件当前项目已更改 55 | self.列表框1.绑定事件当前项目已更改(self.当前项目已更改) 56 | # 绑定事件当前行已更改 57 | self.列表框1.绑定事件当前行已更改(self.当前行已更改) 58 | # 绑定事件当前文本已更改 绑定事件项目已更改 绑定事件项目被双击 绑定事件项目被鼠标进入 绑定事件项目被鼠标按下 59 | self.列表框1.绑定事件当前文本已更改(self.当前文本已更改) 60 | self.列表框1.绑定事件项目已更改(self.项目已更改) 61 | self.列表框1.绑定事件项目被双击(self.项目被双击) 62 | self.列表框1.绑定事件项目被鼠标进入(self.项目被鼠标进入) 63 | self.列表框1.绑定事件项目被鼠标按下(self.项目被鼠标按下) 64 | 65 | self.列表框1.现行选中项 = 2 66 | 67 | 68 | 69 | 70 | def 项目选择已更改(self): 71 | print("项目选择已更改", self.列表框1.取选中项目索引()) 72 | 73 | def 项目被点击(self): 74 | print("项目被点击", self.列表框1.取选中项目索引()) 75 | 76 | def 当前项目已更改(self, 当前选中: QListWidgetItem, 上一个: QListWidgetItem): 77 | print("当前项目已更改", 当前选中.text(), 上一个.text()) 78 | 79 | def 当前行已更改(self, 当前行: int): 80 | print("当前行已更改", 当前行) 81 | 82 | def 当前文本已更改(self, 当前文本: str): 83 | print("当前文本已更改", 当前文本) 84 | 85 | def 项目已更改(self, 项目索引: int, 项目文本: str): 86 | print("项目已更改", 项目索引, 项目文本) 87 | 88 | def 项目被双击(self, 项目索引: int): 89 | print("项目被双击", 项目索引) 90 | 91 | def 项目被鼠标进入(self, 项目索引: int): 92 | print("项目被鼠标进入", 项目索引) 93 | 94 | def 项目被鼠标按下(self, 项目索引: int): 95 | print("项目被鼠标按下", 项目索引) 96 | 97 | 98 | app = QApplication([]) 99 | # 创建窗口 400x400 100 | win = Main() 101 | 102 | sys.exit(app.exec()) 103 | -------------------------------------------------------------------------------- /nanodlna/dlna.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: UTF-8 3 | 4 | import os 5 | import pkgutil 6 | import sys 7 | from xml.sax.saxutils import escape as xmlescape 8 | 9 | if sys.version_info.major == 3: 10 | import urllib.request as urllibreq 11 | else: 12 | import urllib2 as urllibreq 13 | 14 | import traceback 15 | import logging 16 | import json 17 | 18 | 19 | def send_dlna_action(device, data, action): 20 | 21 | logging.debug("Sending DLNA Action: {}".format( 22 | json.dumps({ 23 | "action": action, 24 | "device": device, 25 | "data": data 26 | }) 27 | )) 28 | 29 | action_data = pkgutil.get_data( 30 | "nanodlna", "templates/action-{0}.xml".format(action)).decode("UTF-8") 31 | if data: 32 | action_data = action_data.format(**data) 33 | action_data = action_data.encode("UTF-8") 34 | 35 | headers = { 36 | "Content-Type": "text/xml; charset=\"utf-8\"", 37 | "Content-Length": "{0}".format(len(action_data)), 38 | "Connection": "close", 39 | "SOAPACTION": "\"{0}#{1}\"".format(device["st"], action) 40 | } 41 | 42 | logging.debug("Sending DLNA Request: {}".format( 43 | json.dumps({ 44 | "url": device["action_url"], 45 | "data": action_data.decode("UTF-8"), 46 | "headers": headers 47 | }) 48 | )) 49 | 50 | try: 51 | request = urllibreq.Request(device["action_url"], action_data, headers) 52 | urllibreq.urlopen(request) 53 | logging.debug("Request sent") 54 | except Exception: 55 | logging.error("Unknown error sending request: {}".format( 56 | json.dumps({ 57 | "url": device["action_url"], 58 | "data": action_data.decode("UTF-8"), 59 | "headers": headers, 60 | "error": traceback.format_exc() 61 | }) 62 | )) 63 | 64 | 65 | def play(files_urls, device): 66 | video_data = { 67 | "uri_video": files_urls["file_video"], 68 | "type_video": os.path.splitext(files_urls["file_video"])[1][1:], 69 | } 70 | if "file_subtitle" in files_urls and files_urls["file_subtitle"]: 71 | metadata = pkgutil.get_data( 72 | "nanodlna", 73 | "templates/metadata-video_subtitle.xml").decode("UTF-8") 74 | video_data["metadata"] = xmlescape(metadata.format(**video_data)) 75 | else: 76 | video_data["metadata"] = "" 77 | send_dlna_action(device, video_data, "SetAVTransportURI") 78 | send_dlna_action(device, video_data, "Play") 79 | 80 | 81 | def pause(device): 82 | logging.debug("Pausing device: {}".format( 83 | json.dumps({ 84 | "device": device 85 | }) 86 | )) 87 | send_dlna_action(device, None, "Pause") 88 | 89 | 90 | def stop(device): 91 | logging.debug("Stopping device: {}".format( 92 | json.dumps({ 93 | "device": device 94 | }) 95 | )) 96 | send_dlna_action(device, None, "Stop") 97 | -------------------------------------------------------------------------------- /go2tv_res/internal/gui/about.go: -------------------------------------------------------------------------------- 1 | //go:build !(android || ios) 2 | // +build !android,!ios 3 | 4 | package gui 5 | 6 | import ( 7 | "errors" 8 | "net/http" 9 | "net/url" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "fyne.io/fyne/v2" 16 | "fyne.io/fyne/v2/container" 17 | "fyne.io/fyne/v2/dialog" 18 | "fyne.io/fyne/v2/widget" 19 | ) 20 | 21 | func aboutWindow(s *NewScreen) fyne.CanvasObject { 22 | richhead := widget.NewRichTextFromMarkdown(` 23 | # Go2TV 24 | 25 | Cast your media files to UPnP/DLNA Media Renderers and Smart TVs 26 | 27 | --- 28 | 29 | ## Author 30 | Alex Ballas - alex@ballas.org 31 | 32 | ## License 33 | MIT 34 | 35 | ## Version 36 | 37 | ` + s.version) 38 | 39 | for i := range richhead.Segments { 40 | if seg, ok := richhead.Segments[i].(*widget.TextSegment); ok { 41 | seg.Style.Alignment = fyne.TextAlignCenter 42 | } 43 | if seg, ok := richhead.Segments[i].(*widget.HyperlinkSegment); ok { 44 | seg.Alignment = fyne.TextAlignCenter 45 | } 46 | } 47 | githubbutton := widget.NewButton("Github page", func() { 48 | go func() { 49 | u, _ := url.Parse("https://github.com/alexballas/go2tv") 50 | _ = fyne.CurrentApp().OpenURL(u) 51 | }() 52 | }) 53 | checkversion := widget.NewButton("Check version", func() { 54 | go checkVersion(s) 55 | }) 56 | 57 | s.CheckVersion = checkversion 58 | 59 | return container.NewVBox(richhead, container.NewCenter(container.NewHBox(githubbutton, checkversion))) 60 | } 61 | 62 | func checkVersion(s *NewScreen) { 63 | s.CheckVersion.Disable() 64 | defer s.CheckVersion.Enable() 65 | errRedirectChecker := errors.New("redirect") 66 | errVersioncomp := errors.New("failed to get version info - on develop or non-compiled version") 67 | errVersionGet := errors.New("failed to get version info - check your internet connection") 68 | 69 | str := strings.ReplaceAll(s.version, ".", "") 70 | str = strings.TrimSpace(str) 71 | currversion, err := strconv.Atoi(str) 72 | if err != nil { 73 | dialog.ShowError(errVersioncomp, s.Current) 74 | return 75 | } 76 | 77 | req, err := http.NewRequest("GET", "https://github.com/alexballas/Go2TV/releases/latest", nil) 78 | if err != nil { 79 | dialog.ShowError(errVersionGet, s.Current) 80 | } 81 | 82 | client := &http.Client{ 83 | Timeout: time.Duration(3 * time.Second), 84 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 85 | return errRedirectChecker 86 | }, 87 | } 88 | 89 | response, err := client.Do(req) 90 | if err != nil && !errors.Is(err, errRedirectChecker) { 91 | dialog.ShowError(errVersionGet, s.Current) 92 | return 93 | } 94 | 95 | defer response.Body.Close() 96 | 97 | if errors.Is(err, errRedirectChecker) { 98 | url, err := response.Location() 99 | if err != nil { 100 | dialog.ShowError(errVersionGet, s.Current) 101 | return 102 | } 103 | str := strings.Trim(filepath.Base(url.Path), "v") 104 | str = strings.ReplaceAll(str, ".", "") 105 | chversion, err := strconv.Atoi(str) 106 | if err != nil { 107 | dialog.ShowError(errVersionGet, s.Current) 108 | return 109 | } 110 | 111 | switch { 112 | case chversion > currversion: 113 | dialog.ShowInformation("Version checker", "New version: "+strings.Trim(filepath.Base(url.Path), "v"), s.Current) 114 | return 115 | default: 116 | dialog.ShowInformation("Version checker", "No new version", s.Current) 117 | return 118 | } 119 | } 120 | 121 | dialog.ShowError(errVersionGet, s.Current) 122 | } 123 | -------------------------------------------------------------------------------- /go2tv_res/internal/gui/about_mobile.go: -------------------------------------------------------------------------------- 1 | //go:build android || ios 2 | // +build android ios 3 | 4 | package gui 5 | 6 | import ( 7 | "errors" 8 | "net/http" 9 | "net/url" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "fyne.io/fyne/v2" 16 | "fyne.io/fyne/v2/container" 17 | "fyne.io/fyne/v2/dialog" 18 | "fyne.io/fyne/v2/widget" 19 | ) 20 | 21 | func aboutWindow(s *NewScreen) fyne.CanvasObject { 22 | richhead := widget.NewRichTextFromMarkdown(` 23 | # Go2TV 24 | 25 | Cast your media files to UPnP/DLNA 26 | 27 | Media Renderers and Smart TVs 28 | 29 | --- 30 | 31 | ## Author 32 | Alex Ballas - alex@ballas.org 33 | 34 | ## License 35 | MIT 36 | 37 | ## Version 38 | 39 | ` + s.version) 40 | 41 | for i := range richhead.Segments { 42 | if seg, ok := richhead.Segments[i].(*widget.TextSegment); ok { 43 | seg.Style.Alignment = fyne.TextAlignCenter 44 | } 45 | if seg, ok := richhead.Segments[i].(*widget.HyperlinkSegment); ok { 46 | seg.Alignment = fyne.TextAlignCenter 47 | } 48 | } 49 | githubbutton := widget.NewButton("Github page", func() { 50 | go func() { 51 | u, _ := url.Parse("https://github.com/alexballas/go2tv") 52 | _ = fyne.CurrentApp().OpenURL(u) 53 | }() 54 | }) 55 | checkversion := widget.NewButton("Check version", func() { 56 | go checkVersion(s) 57 | }) 58 | 59 | s.CheckVersion = checkversion 60 | 61 | return container.NewVBox(richhead, container.NewCenter(container.NewHBox(githubbutton, checkversion))) 62 | } 63 | 64 | func checkVersion(s *NewScreen) { 65 | s.CheckVersion.Disable() 66 | defer s.CheckVersion.Enable() 67 | errRedirectChecker := errors.New("redirect") 68 | errVersioncomp := errors.New("failed to get version info\non develop or non-compiled version") 69 | errVersionGet := errors.New("failed to get version info\ncheck your internet connection") 70 | 71 | str := strings.ReplaceAll(s.version, ".", "") 72 | str = strings.TrimSpace(str) 73 | currversion, err := strconv.Atoi(str) 74 | if err != nil { 75 | dialog.ShowError(errVersioncomp, s.Current) 76 | return 77 | } 78 | 79 | req, err := http.NewRequest("GET", "https://github.com/alexballas/Go2TV/releases/latest", nil) 80 | if err != nil { 81 | dialog.ShowError(errVersionGet, s.Current) 82 | } 83 | 84 | client := &http.Client{ 85 | Timeout: time.Duration(3 * time.Second), 86 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 87 | return errRedirectChecker 88 | }, 89 | } 90 | 91 | response, err := client.Do(req) 92 | if err != nil && !errors.Is(err, errRedirectChecker) { 93 | dialog.ShowError(errVersionGet, s.Current) 94 | return 95 | } 96 | 97 | defer response.Body.Close() 98 | 99 | if errors.Is(err, errRedirectChecker) { 100 | url, err := response.Location() 101 | if err != nil { 102 | dialog.ShowError(errVersionGet, s.Current) 103 | return 104 | } 105 | str := strings.Trim(filepath.Base(url.Path), "v") 106 | str = strings.ReplaceAll(str, ".", "") 107 | chversion, err := strconv.Atoi(str) 108 | if err != nil { 109 | dialog.ShowError(errVersionGet, s.Current) 110 | return 111 | } 112 | 113 | switch { 114 | case chversion > currversion: 115 | dialog.ShowInformation("Version checker", "New version: "+strings.Trim(filepath.Base(url.Path), "v"), s.Current) 116 | return 117 | default: 118 | dialog.ShowInformation("Version checker", "No new version", s.Current) 119 | return 120 | } 121 | } 122 | 123 | dialog.ShowError(errVersionGet, s.Current) 124 | } 125 | -------------------------------------------------------------------------------- /nanodlna/streaming.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: UTF-8 3 | 4 | import os 5 | import socket 6 | import threading 7 | import unicodedata 8 | import re 9 | 10 | from twisted.internet import reactor 11 | from twisted.web.resource import Resource 12 | from twisted.web.server import Site 13 | from twisted.web.static import File 14 | 15 | import logging 16 | import json 17 | 18 | # from twisted.python import log 19 | 20 | 21 | def normalize_file_name(value): 22 | value = unicodedata\ 23 | .normalize("NFKD", value)\ 24 | .encode("ascii", "ignore")\ 25 | .decode("ascii") 26 | value = re.sub(r"[^\.\w\s-]", "", value.lower()) 27 | value = re.sub(r"[-\s]+", "-", value).strip("-_") 28 | return value 29 | 30 | 31 | def set_files(files, serve_ip, serve_port): 32 | 33 | logging.debug("Setting streaming files: {}".format( 34 | json.dumps({ 35 | "files": files, 36 | "serve_ip": serve_ip, 37 | "serve_port": serve_port 38 | }) 39 | )) 40 | 41 | files_index = {file_key: (normalize_file_name(os.path.basename(file_path)), 42 | os.path.abspath(file_path), 43 | os.path.dirname(os.path.abspath(file_path))) 44 | for file_key, file_path in files.items()} 45 | 46 | files_serve = {file_name: file_path 47 | for file_name, file_path, file_dir in files_index.values()} 48 | 49 | files_urls = { 50 | file_key: "http://{0}:{1}/{2}/{3}".format( 51 | serve_ip, serve_port, file_key, file_name) 52 | for file_key, (file_name, file_path, file_dir) 53 | in files_index.items()} 54 | 55 | logging.debug("Streaming files information: {}".format( 56 | json.dumps({ 57 | "files_index": files_index, 58 | "files_serve": files_serve, 59 | "files_urls": files_urls 60 | }) 61 | )) 62 | 63 | return files_index, files_serve, files_urls 64 | 65 | 66 | def start_server(files, serve_ip, serve_port=9000): 67 | 68 | # import sys 69 | # log.startLogging(sys.stdout) 70 | 71 | logging.debug("Starting to create streaming server") 72 | 73 | files_index, files_serve, files_urls = set_files( 74 | files, serve_ip, serve_port) 75 | 76 | logging.debug("Adding files to HTTP server") 77 | root = Resource() 78 | for file_key, (file_name, file_path, file_dir) in files_index.items(): 79 | root.putChild(file_key.encode("utf-8"), Resource()) 80 | root.children[file_key.encode("utf-8")].putChild( 81 | file_name.encode("utf-8"), File(file_path)) 82 | 83 | logging.debug("Starting to listen messages in HTTP server") 84 | 85 | reactor.listenTCP(serve_port, Site(root)) 86 | threading.Thread( 87 | target=reactor.run, kwargs={"installSignalHandlers": False}).start() 88 | 89 | return files_urls 90 | 91 | 92 | def stop_server(): 93 | reactor.stop() 94 | 95 | 96 | def get_serve_ip(target_ip, target_port=80): 97 | logging.debug("Identifying server IP") 98 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 99 | s.connect((target_ip, target_port)) 100 | serve_ip = s.getsockname()[0] 101 | s.close() 102 | logging.debug("Server IP identified: {}".format(serve_ip)) 103 | return serve_ip 104 | 105 | 106 | if __name__ == "__main__": 107 | 108 | import sys 109 | 110 | files = {"file_{0}".format(i): file_path for i, 111 | file_path in enumerate(sys.argv[1:], 1)} 112 | print(files) 113 | 114 | files_urls = start_server(files, "localhost") 115 | print(files_urls) 116 | -------------------------------------------------------------------------------- /qtefun/组件/列表框.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | 3 | from qtefun.组件.组件公共类 import 组件公共类 4 | 5 | 6 | # https://doc.qt.io/qt-6/qlistwidget.html 7 | # 列表框 添加项目 删除项目 取项目数量 取项目文本 取项目索引 取当前选中项目索引 取当前选中项目文本 8 | # 事件 9 | # void currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) # 当前选中项目改变时触发 10 | # void currentRowChanged(int currentRow) # 当前选中项目索引改变时触发 11 | # void currentTextChanged(const QString ¤tText) # 当前选中项目改变时触发 12 | # void itemActivated(QListWidgetItem *item) # 项目被点击时触发 13 | # void itemChanged(QListWidgetItem *item) # 项目改变时触发 14 | # void itemClicked(QListWidgetItem *item) # 项目被点击时触发 15 | # void itemDoubleClicked(QListWidgetItem *item) # 项目被双击时触发 16 | # void itemEntered(QListWidgetItem *item) # 项目被鼠标进入时触发 17 | # void itemPressed(QListWidgetItem *item) # 项目被按下时触发 18 | # void itemSelectionChanged() # 选中项目改变时触发 19 | 20 | class 列表框(组件公共类): 21 | 对象 = None # type: QtWidgets.QListWidget 22 | 23 | # 添加项目 24 | def 添加项目(self, 文本: str): 25 | return self.对象.addItem(文本) 26 | 27 | # 删除项目 28 | def 删除项目(self, 索引: int): 29 | return self.对象.takeItem(索引) 30 | 31 | # 插入项目 32 | def 插入项目(self, 索引: int, 文本: str): 33 | return self.对象.insertItem(索引, 文本) 34 | 35 | # 取项目数量 36 | def 取项目数量(self): 37 | return self.对象.count() 38 | 39 | # 取项目文本 40 | def 取项目文本(self, 索引: int): 41 | return self.对象.item(索引).text() 42 | 43 | # 取项目索引 44 | def 取项目索引(self, 文本: str): 45 | for i in range(self.对象.count()): 46 | if self.对象.item(i).text() == 文本: 47 | return i 48 | return -1 49 | 50 | # 取选中项目索引 51 | def 取选中项目索引(self): 52 | return self.对象.currentRow() 53 | 54 | # 设置选中项目索引 55 | def 设置选中项目索引(self, 索引: int): 56 | return self.对象.setCurrentRow(索引) 57 | 58 | def 绑定事件当前项目已更改(self, 回调函数): 59 | """ 60 | 回调函数(当前选中:QListWidgetItem, 上一个:QListWidgetItem) 61 | """ 62 | self.对象.currentItemChanged.connect(回调函数) 63 | 64 | def 绑定事件当前行已更改(self, 回调函数): 65 | """ 66 | 回调函数(当前行:int) 67 | """ 68 | self.对象.currentRowChanged.connect(回调函数) 69 | 70 | # void currentTextChanged(const QString ¤tText) # 当前选中项目改变时触发 71 | def 绑定事件当前文本已更改(self, 回调函数): 72 | """ 73 | 回调函数(当前文本:str) 74 | """ 75 | self.对象.currentTextChanged.connect(回调函数) 76 | 77 | # void itemActivated(QListWidgetItem *item) # 项目被点击时触发 78 | def 绑定事件项目被点击(self, 回调函数): 79 | """ 80 | 回调函数(项目:QListWidgetItem) 81 | """ 82 | self.对象.itemActivated.connect(回调函数) 83 | 84 | # void itemChanged(QListWidgetItem *item) # 项目改变时触发 85 | def 绑定事件项目已更改(self, 回调函数): 86 | """ 87 | 回调函数(项目:QListWidgetItem) 88 | """ 89 | self.对象.itemChanged.connect(回调函数) 90 | 91 | # void itemClicked(QListWidgetItem *item) # 项目被点击时触发 92 | def 绑定事件项目被双击(self, 回调函数): 93 | """ 94 | 回调函数(项目:QListWidgetItem) 95 | """ 96 | self.对象.itemDoubleClicked.connect(回调函数) 97 | 98 | # void itemEntered(QListWidgetItem *item) # 项目被鼠标进入时触发 99 | def 绑定事件项目被鼠标进入(self, 回调函数): 100 | """ 101 | 回调函数(项目:QListWidgetItem) 102 | """ 103 | self.对象.itemEntered.connect(回调函数) 104 | 105 | # void itemPressed(QListWidgetItem *item) # 项目被鼠标按下时触发 106 | def 绑定事件项目被鼠标按下(self, 回调函数): 107 | """ 108 | 回调函数(项目:QListWidgetItem) 109 | """ 110 | self.对象.itemPressed.connect(回调函数) 111 | 112 | # void itemSelectionChanged() # 选中项目改变时触发 113 | def 绑定事件选中项目已更改(self, 回调函数): 114 | self.对象.itemSelectionChanged.connect(回调函数) 115 | 116 | # 获取和设置属性 现行选中项 117 | @property 118 | def 现行选中项(self): 119 | return self.取选中项目索引() 120 | 121 | @现行选中项.setter 122 | def 现行选中项(self, 索引: int): 123 | return self.设置选中项目索引(索引) 124 | -------------------------------------------------------------------------------- /qtefun/组件/表格.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | from PySide6.QtWidgets import QTableWidgetItem, QAbstractItemView, QLineEdit, QComboBox, QPushButton, QCheckBox 3 | 4 | from qtefun.组件.组件公共类 import 组件公共类 5 | 6 | 7 | # https://doc.qt.io/qt-6/qtreewidget.html 8 | # 表格 9 | # 绑定事件 10 | class 表格节点类(QTableWidgetItem): 11 | 12 | def 置标题(self, 第几列, 标题): 13 | self.setText(第几列, 标题) 14 | 15 | def 置标题多列(self, 标题列表, 从第几列开始=0): 16 | i = 从第几列开始 17 | for 标题 in 标题列表: 18 | self.setText(i, 标题) 19 | i = i + 1 20 | 21 | 22 | class 表格(组件公共类): 23 | 对象 = None # type: QtWidgets.QTableWidget 24 | 25 | # 设置列数 26 | def 设置列数(self, 列数): 27 | self.对象.setColumnCount(列数) 28 | 29 | # 设置行数 30 | def 设置行数(self, 行数): 31 | self.对象.setRowCount(行数) 32 | 33 | # 设置表头 34 | def 设置表头(self, 表头=[]): 35 | self.对象.setHorizontalHeaderLabels(表头) 36 | 37 | # 设置整行选择 38 | def 设置整行选择(self): 39 | self.对象.setSelectionBehavior(QAbstractItemView.SelectRows) 40 | 41 | # 设置单一选择 42 | def 设置单一选择(self): 43 | self.对象.setSelectionMode(QAbstractItemView.SingleSelection) 44 | 45 | # 设置不可编辑 46 | def 设置不可编辑(self): 47 | self.对象.setEditTriggers(QAbstractItemView.NoEditTriggers) 48 | 49 | def 插入行(self, 行号): 50 | self.对象.insertRow(行号) 51 | 52 | def 插入列(self, 列号): 53 | self.对象.insertColumn(列号) 54 | 55 | def 设置项目文本(self, 行号, 列号, 文本): 56 | self.对象.setItem(行号, 列号, 表格节点类(文本)) 57 | 58 | def 设置单元格组件(self, 行号, 列号, 组件): 59 | self.对象.setCellWidget(行号, 列号, 组件) 60 | 61 | def 设置单元格文本框(self, 行号, 列号, 初始文本: str, 回调函数): 62 | inputText = QLineEdit() 63 | inputText.setText(初始文本) 64 | 65 | def 完成编辑(): 66 | 回调函数(行号, 列号, inputText.text()) 67 | 68 | inputText.editingFinished.connect(完成编辑) 69 | self.设置单元格组件(行号, 列号, inputText) 70 | 71 | def 设置单元格复选框(self, 行号, 列号, 初始状态: bool, 初始文本: str, 回调函数): 72 | checkBox = QtWidgets.QCheckBox() 73 | checkBox.setText(初始文本) 74 | checkBox.setChecked(初始状态) 75 | 76 | # 绑定选择事件 77 | def 选择事件(): 78 | 回调函数(行号, 列号, checkBox.isChecked()) 79 | 80 | checkBox.stateChanged.connect(选择事件) 81 | self.设置单元格组件(行号, 列号, checkBox) 82 | 83 | def 设置单元格组合框(self, 行号, 列号, 项目列表: list, 默认选择索引, 回调函数): 84 | if 默认选择索引 is None: 85 | 默认选择索引 = 0 86 | 87 | comboBox = QComboBox() 88 | for item in 项目列表: 89 | comboBox.addItem(item) 90 | 91 | def 选择项目(): 92 | 回调函数(行号, 列号, comboBox.currentText()) 93 | 94 | comboBox.setCurrentIndex(默认选择索引) 95 | comboBox.activated.connect(选择项目) 96 | self.设置单元格组件(行号, 列号, comboBox) 97 | 98 | # 设置单元格按钮 99 | def 设置单元格按钮(self, 行号, 列号, 文本, 回调函数): 100 | button = QPushButton(self.对象) 101 | button.setText(文本) 102 | 103 | def 被点击(): 104 | 回调函数(行号, 列号) 105 | 106 | button.clicked.connect(被点击) 107 | self.设置单元格组件(行号, 列号, button) 108 | 109 | def 取行数(self): 110 | return self.对象.rowCount() 111 | 112 | def 取列数(self): 113 | return self.对象.columnCount() 114 | 115 | def 导出数据(self): 116 | 整体数据 = [] 117 | for x in range(self.取行数()): 118 | 组合数据 = [] 119 | for y in range(self.取列数()): 120 | obj = self.对象.cellWidget(x, y) 121 | if obj is None: 122 | 数据 = self.对象.item(x, y).text() 123 | else: 124 | 数据 = None 125 | if isinstance(obj, QLineEdit): 126 | 数据 = obj.text() 127 | elif isinstance(obj, QCheckBox): 128 | 数据 = obj.isChecked() 129 | elif isinstance(obj, QComboBox): 130 | 数据 = obj.currentText() 131 | 组合数据.append(数据) 132 | 整体数据.append(组合数据) 133 | return 整体数据 134 | -------------------------------------------------------------------------------- /go2tv_res/utils/dlnatools.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/h2non/filetype" 10 | ) 11 | 12 | const ( 13 | // dlnaOrgFlagSenderPaced = 1 << 31 14 | // dlnaOrgFlagTimeBasedSeek = 1 << 30 15 | // dlnaOrgFlagByteBasedSeek = 1 << 29 16 | // dlnaOrgFlagPlayContainer = 1 << 28 17 | // dlnaOrgFlagS0Increase = 1 << 27 18 | // dlnaOrgFlagSnIncrease = 1 << 26 19 | // dlnaOrgFlagRtspPause = 1 << 25 20 | dlnaOrgFlagStreamingTransferMode = 1 << 24 21 | // dlnaOrgFlagInteractiveTransfertMode = 1 << 23 22 | dlnaOrgFlagBackgroundTransfertMode = 1 << 22 23 | dlnaOrgFlagConnectionStall = 1 << 21 24 | dlnaOrgFlagDlnaV15 = 1 << 20 25 | ) 26 | 27 | var ( 28 | dlnaprofiles = map[string]string{ 29 | "video/x-mkv": "DLNA.ORG_PN=MATROSKA", 30 | "video/x-matroska": "DLNA.ORG_PN=MATROSKA", 31 | "video/x-msvideo": "DLNA.ORG_PN=AVI", 32 | "video/mpeg": "DLNA.ORG_PN=MPEG1", 33 | "video/vnd.dlna.mpeg-tts": "DLNA.ORG_PN=MPEG1", 34 | "video/mp4": "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5", 35 | "video/quicktime": "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5", 36 | "video/x-m4v": "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5", 37 | "video/3gpp": "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5", 38 | "video/x-flv": "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5", 39 | "video/x-ms-wmv": "DLNA.ORG_PN=WMVHIGH_FULL", 40 | "audio/mpeg": "DLNA.ORG_PN=MP3", 41 | "image/jpeg": "JPEG_LRG", 42 | "image/png": "PNG_LRG", 43 | } 44 | ) 45 | 46 | func defaultStreamingFlags() string { 47 | return fmt.Sprintf("%.8x%.24x", dlnaOrgFlagStreamingTransferMode| 48 | dlnaOrgFlagBackgroundTransfertMode| 49 | dlnaOrgFlagConnectionStall| 50 | dlnaOrgFlagDlnaV15, 0) 51 | } 52 | 53 | // BuildContentFeatures builds the content features string 54 | // for the "contentFeatures.dlna.org" header. 55 | func BuildContentFeatures(mediaType string, seek string, transcode bool) (string, error) { 56 | var cf strings.Builder 57 | 58 | if mediaType != "" { 59 | dlnaProf, profExists := dlnaprofiles[mediaType] 60 | if profExists { 61 | cf.WriteString(dlnaProf + ";") 62 | } 63 | } 64 | 65 | // "00" neither time seek range nor range supported 66 | // "01" range supported 67 | // "10" time seek range supported 68 | // "11" both time seek range and range supported 69 | switch seek { 70 | case "00": 71 | cf.WriteString("DLNA.ORG_OP=00;") 72 | case "01": 73 | cf.WriteString("DLNA.ORG_OP=01;") 74 | case "10": 75 | cf.WriteString("DLNA.ORG_OP=10;") 76 | case "11": 77 | cf.WriteString("DLNA.ORG_OP=11;") 78 | default: 79 | return "", errors.New("invalid seek flag") 80 | } 81 | 82 | switch transcode { 83 | case true: 84 | cf.WriteString("DLNA.ORG_CI=1;") 85 | default: 86 | cf.WriteString("DLNA.ORG_CI=0;") 87 | } 88 | 89 | cf.WriteString("DLNA.ORG_FLAGS=") 90 | cf.WriteString(defaultStreamingFlags()) 91 | 92 | return cf.String(), nil 93 | } 94 | 95 | // GetMimeDetailsFromFile returns the media file mime details. 96 | func GetMimeDetailsFromFile(f io.ReadCloser) (string, error) { 97 | defer f.Close() 98 | head := make([]byte, 261) 99 | _, err := f.Read(head) 100 | if err != nil { 101 | return "", fmt.Errorf("getMimeDetailsFromFile error #2: %w", err) 102 | } 103 | 104 | kind, err := filetype.Match(head) 105 | if err != nil { 106 | return "", fmt.Errorf("getMimeDetailsFromFile error #3: %w", err) 107 | } 108 | 109 | return fmt.Sprintf("%s/%s", kind.MIME.Type, kind.MIME.Subtype), nil 110 | } 111 | 112 | // GetMimeDetailsFromStream returns the media URL mime details. 113 | func GetMimeDetailsFromStream(s io.ReadCloser) (string, error) { 114 | defer s.Close() 115 | head := make([]byte, 261) 116 | _, err := s.Read(head) 117 | if err != nil { 118 | return "", fmt.Errorf("getMimeDetailsFromStream error: %w", err) 119 | } 120 | 121 | kind, err := filetype.Match(head) 122 | if err != nil { 123 | return "", fmt.Errorf("getMimeDetailsFromStream error #2: %w", err) 124 | } 125 | 126 | return fmt.Sprintf("%s/%s", kind.MIME.Type, kind.MIME.Subtype), nil 127 | } 128 | -------------------------------------------------------------------------------- /go2tv_res/assets/go2tv-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /go2tv_res/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Go2TV logo 4 | 5 |

6 |
7 |
8 |

9 | 10 | ![Go](https://github.com/alexballas/Go2TV/workflows/Go/badge.svg) 11 | [![Go Report Card](https://goreportcard.com/badge/github.com/alexballas/Go2TV)](https://goreportcard.com/report/github.com/alexballas/Go2TV) 12 | [![Release Version](https://img.shields.io/github/v/release/alexballas/Go2TV?label=Release)](https://github.com/alexballas/Go2TV/releases/latest) 13 | [![Tests](https://github.com/alexballas/go2tv/actions/workflows/go.yml/badge.svg)](https://github.com/alexballas/go2tv/actions/workflows/go.yml) 14 | 15 | [![Build for ARM](https://github.com/alexballas/go2tv/actions/workflows/build-arm.yml/badge.svg)](https://github.com/alexballas/go2tv/actions/workflows/build-arm.yml) 16 | [![Build for Android](https://github.com/alexballas/go2tv/actions/workflows/build-android.yml/badge.svg)](https://github.com/alexballas/go2tv/actions/workflows/build-android.yml) 17 | [![Build for Linux](https://github.com/alexballas/go2tv/actions/workflows/build-linux.yml/badge.svg)](https://github.com/alexballas/go2tv/actions/workflows/build-linux.yml) 18 | [![Build for MacOS](https://github.com/alexballas/go2tv/actions/workflows/build-mac.yml/badge.svg)](https://github.com/alexballas/go2tv/actions/workflows/build-mac.yml) 19 | [![Build for Windows](https://github.com/alexballas/go2tv/actions/workflows/build-windows.yml/badge.svg)](https://github.com/alexballas/go2tv/actions/workflows/build-windows.yml) 20 |

21 | Cast your media files to UPnP/DLNA Media Renderers and Smart TVs. 22 |
23 | 24 | --- 25 | GUI mode 26 | ----- 27 | ![](https://i.imgur.com/nrfIc81.png) 28 | ![](https://i.imgur.com/ksCaCFl.png) 29 | 30 | CLI mode 31 | ----- 32 | ![](https://i.imgur.com/BsMevHi.gif) 33 | 34 | Parameters 35 | ----- 36 | ``` console 37 | $ go2tv -h 38 | Usage of go2tv: 39 | -l List all available UPnP/DLNA Media Renderer models and URLs. 40 | -s string 41 | Local path to the subtitles file. 42 | -t string 43 | Cast to a specific UPnP/DLNA Media Renderer URL. 44 | -tc 45 | Use ffmpeg to transcode input video file. 46 | -u string 47 | HTTP URL to the media file. URL streaming does not support seek operations. (Triggers the CLI mode) 48 | -v string 49 | Local path to the video/audio file. (Triggers the CLI mode) 50 | -version 51 | Print version. 52 | ``` 53 | 54 | Allowed media files in the GUI 55 | ----- 56 | - mp4, avi, mkv, mpeg, mov, webm, m4v, mpv, mp3, flac, wav, jpg, jpeg, png 57 | 58 | This is a GUI only limitation. 59 | 60 | Build requirements and dependencies 61 | ----- 62 | - Go v1.16+ 63 | - ffmpeg (optional) 64 | 65 | **Build using Docker** 66 | 67 | Since the repo provides a [Dockerfile](./Dockerfile), you can build a Go2TV Docker image and run it with just Docker installed (no build requirements and deps above needed). Also, no Git repo cloning is needed (Docker will do it behind the scenes). Just issue: 68 | ``` console 69 | $ docker build --force-rm [--pull] -t go2tv github.com/alexballas/go2tv#main 70 | ``` 71 | Notice the branch name after the `#`, as the above will build `main`. You can also build `devel` if you want to build the latest code. Usage under Docker is outside this document's scope, check Docker docs for more information, specially volume mounts and networking. [x11docker](https://github.com/mviereck/x11docker) might come handy to run GUI mode, although it's not tested, since main Docker usage is CLI. 72 | 73 | Quick Start 74 | ----- 75 | Download the app here https://github.com/alexballas/Go2TV/releases/latest. A single executable. No installation or external dependencies. 76 | 77 | **Transcoding** 78 | 79 | Go2TV supports live video transcoding, if ffmpeg is installed. When transcoding, SEEK operations are not available. Transcoding offers the maximum compatibility with the various file formats and devices. Only works with video files. 80 | 81 | **MacOS potential issue** 82 | 83 | If you get the "cannot be opened because the developer cannot be verified" error, you can apply the following workaround. 84 | - Control-click the app icon, then choose Open from the shortcut menu. 85 | - Click Open. 86 | 87 | Tested on 88 | ----- 89 | - Samsung UE50JU6400 90 | - Samsung UE65KS7000 91 | - Android - BubbleUPnP app 92 | 93 | Author 94 | ------ 95 | 96 | Alexandros Ballas 97 | -------------------------------------------------------------------------------- /go2tv_res/soapcalls/xmlparsers.go: -------------------------------------------------------------------------------- 1 | package soapcalls 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type rootNode struct { 15 | XMLName xml.Name `xml:"root"` 16 | Device struct { 17 | XMLName xml.Name `xml:"device"` 18 | ServiceList struct { 19 | XMLName xml.Name `xml:"serviceList"` 20 | Services []struct { 21 | XMLName xml.Name `xml:"service"` 22 | Type string `xml:"serviceType"` 23 | ID string `xml:"serviceId"` 24 | ControlURL string `xml:"controlURL"` 25 | EventSubURL string `xml:"eventSubURL"` 26 | } `xml:"service"` 27 | } `xml:"serviceList"` 28 | } `xml:"device"` 29 | } 30 | 31 | type eventPropertySet struct { 32 | XMLName xml.Name `xml:"propertyset"` 33 | EventInstance struct { 34 | XMLName xml.Name `xml:"InstanceID"` 35 | Value string `xml:"val,attr"` 36 | EventCurrentTransportActions struct { 37 | Value string `xml:"val,attr"` 38 | } `xml:"CurrentTransportActions"` 39 | EventTransportState struct { 40 | Value string `xml:"val,attr"` 41 | } `xml:"TransportState"` 42 | } `xml:"property>LastChange>Event>InstanceID"` 43 | } 44 | 45 | // DMRextracted stored the services urls 46 | type DMRextracted struct { 47 | AvtransportControlURL string 48 | AvtransportEventSubURL string 49 | RenderingControlURL string 50 | } 51 | 52 | // DMRextractor extracts the services URLs from the main DMR xml. 53 | func DMRextractor(dmrurl string) (*DMRextracted, error) { 54 | var root rootNode 55 | ex := &DMRextracted{} 56 | 57 | parsedURL, err := url.Parse(dmrurl) 58 | if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { 59 | return nil, fmt.Errorf("DMRextractor parse error: %w", err) 60 | } 61 | 62 | client := &http.Client{} 63 | req, err := http.NewRequest("GET", dmrurl, nil) 64 | if err != nil { 65 | return nil, fmt.Errorf("DMRextractor GET error: %w", err) 66 | } 67 | 68 | req.Header.Set("Connection", "close") 69 | 70 | xmlresp, err := client.Do(req) 71 | if err != nil { 72 | return nil, fmt.Errorf("DMRextractor Do GET error: %w", err) 73 | } 74 | defer xmlresp.Body.Close() 75 | 76 | xmlbody, err := io.ReadAll(xmlresp.Body) 77 | if err != nil { 78 | return nil, fmt.Errorf("DMRextractor read error: %w", err) 79 | } 80 | 81 | err = xml.Unmarshal(xmlbody, &root) 82 | if err != nil { 83 | return nil, fmt.Errorf("DMRextractor unmarshal error: %w", err) 84 | } 85 | 86 | for i := 0; i < len(root.Device.ServiceList.Services); i++ { 87 | service := root.Device.ServiceList.Services[i] 88 | if !strings.HasPrefix(service.EventSubURL, "/") { 89 | service.EventSubURL = "/" + service.EventSubURL 90 | } 91 | if !strings.HasPrefix(service.ControlURL, "/") { 92 | service.ControlURL = "/" + service.ControlURL 93 | } 94 | 95 | if service.ID == "urn:upnp-org:serviceId:AVTransport" { 96 | ex.AvtransportControlURL = parsedURL.Scheme + "://" + parsedURL.Host + service.ControlURL 97 | ex.AvtransportEventSubURL = parsedURL.Scheme + "://" + parsedURL.Host + service.EventSubURL 98 | 99 | _, err := url.ParseRequestURI(ex.AvtransportControlURL) 100 | if err != nil { 101 | return nil, fmt.Errorf("DMRextractor invalid AvtransportControlURL: %w", err) 102 | } 103 | 104 | _, err = url.ParseRequestURI(ex.AvtransportEventSubURL) 105 | if err != nil { 106 | return nil, fmt.Errorf("DMRextractor invalid AvtransportEventSubURL: %w", err) 107 | } 108 | } 109 | if service.ID == "urn:upnp-org:serviceId:RenderingControl" { 110 | ex.RenderingControlURL = parsedURL.Scheme + "://" + parsedURL.Host + service.ControlURL 111 | 112 | _, err = url.ParseRequestURI(ex.RenderingControlURL) 113 | if err != nil { 114 | return nil, fmt.Errorf("DMRextractor invalid RenderingControlURL: %w", err) 115 | } 116 | } 117 | } 118 | 119 | if ex.AvtransportControlURL != "" { 120 | return ex, nil 121 | } 122 | 123 | return nil, errors.New("something broke somewhere - wrong DMR URL?") 124 | } 125 | 126 | // EventNotifyParser parses the Notify messages from the DMR device. 127 | func EventNotifyParser(xmlbody string) (string, string, error) { 128 | var root eventPropertySet 129 | err := xml.Unmarshal([]byte(xmlbody), &root) 130 | if err != nil { 131 | return "", "", fmt.Errorf("EventNotifyParser unmarshal error: %w", err) 132 | } 133 | previousstate := root.EventInstance.EventCurrentTransportActions.Value 134 | newstate := root.EventInstance.EventTransportState.Value 135 | 136 | return previousstate, newstate, nil 137 | } 138 | -------------------------------------------------------------------------------- /go2tv_res/internal/gui/gui_mobile.go: -------------------------------------------------------------------------------- 1 | //go:build android || ios 2 | // +build android ios 3 | 4 | package gui 5 | 6 | import ( 7 | "os" 8 | "strings" 9 | "sync" 10 | 11 | "fyne.io/fyne/v2" 12 | "fyne.io/fyne/v2/app" 13 | "fyne.io/fyne/v2/container" 14 | "fyne.io/fyne/v2/dialog" 15 | "fyne.io/fyne/v2/theme" 16 | "fyne.io/fyne/v2/widget" 17 | "github.com/alexballas/go2tv/httphandlers" 18 | "github.com/alexballas/go2tv/soapcalls" 19 | "github.com/pkg/errors" 20 | ) 21 | 22 | // NewScreen . 23 | type NewScreen struct { 24 | mu sync.RWMutex 25 | Current fyne.Window 26 | tvdata *soapcalls.TVPayload 27 | Stop *widget.Button 28 | MuteUnmute *widget.Button 29 | CheckVersion *widget.Button 30 | CustomSubsCheck *widget.Check 31 | ExternalMediaURL *widget.Check 32 | MediaText *widget.Entry 33 | SubsText *widget.Entry 34 | DeviceList *widget.List 35 | httpserver *httphandlers.HTTPserver 36 | PlayPause *widget.Button 37 | mediafile fyne.URI 38 | subsfile fyne.URI 39 | selectedDevice devType 40 | State string 41 | controlURL string 42 | eventlURL string 43 | renderingControlURL string 44 | version string 45 | mediaFormats []string 46 | Medialoop bool 47 | } 48 | 49 | type devType struct { 50 | name string 51 | addr string 52 | } 53 | 54 | type mainButtonsLayout struct { 55 | buttonHeight float32 56 | } 57 | 58 | // Start . 59 | func Start(s *NewScreen) { 60 | w := s.Current 61 | 62 | tabs := container.NewAppTabs( 63 | container.NewTabItem("Go2TV", container.NewVScroll(container.NewPadded(mainWindow(s)))), 64 | container.NewTabItem("About", container.NewVScroll(aboutWindow(s))), 65 | ) 66 | 67 | w.SetContent(tabs) 68 | w.CenterOnScreen() 69 | w.ShowAndRun() 70 | os.Exit(0) 71 | } 72 | 73 | // EmitMsg Method to implement the screen interface 74 | func (p *NewScreen) EmitMsg(a string) { 75 | switch a { 76 | case "Playing": 77 | setPlayPauseView("Pause", p) 78 | p.updateScreenState("Playing") 79 | case "Paused": 80 | setPlayPauseView("Play", p) 81 | p.updateScreenState("Paused") 82 | case "Stopped": 83 | setPlayPauseView("Play", p) 84 | p.updateScreenState("Stopped") 85 | stopAction(p) 86 | default: 87 | dialog.ShowInformation("?", "Unknown callback value", p.Current) 88 | } 89 | } 90 | 91 | // Fini Method to implement the screen interface. 92 | // Will only be executed when we receive a callback message, 93 | // not when we explicitly click the Stop button. 94 | func (p *NewScreen) Fini() { 95 | // Main media loop logic 96 | if p.Medialoop { 97 | playAction(p) 98 | } 99 | } 100 | 101 | // InitFyneNewScreen . 102 | func InitFyneNewScreen(v string) *NewScreen { 103 | go2tv := app.NewWithID("com.alexballas.go2tv") 104 | go2tv.Settings().SetTheme(theme.DarkTheme()) 105 | 106 | w := go2tv.NewWindow("Go2TV") 107 | 108 | return &NewScreen{ 109 | Current: w, 110 | mediaFormats: []string{".mp4", ".avi", ".mkv", ".mpeg", ".mov", ".webm", ".m4v", ".mpv", ".mp3", ".flac", ".wav"}, 111 | version: v, 112 | } 113 | } 114 | 115 | func check(win fyne.Window, err error) { 116 | if err != nil { 117 | cleanErr := strings.ReplaceAll(err.Error(), ": ", "\n") 118 | dialog.ShowError(errors.New(cleanErr), win) 119 | } 120 | } 121 | 122 | // updateScreenState updates the screen state based on 123 | // the emitted messages. The State variable is used across 124 | // the GUI interface to control certain flows. 125 | func (p *NewScreen) updateScreenState(a string) { 126 | p.mu.Lock() 127 | p.State = a 128 | p.mu.Unlock() 129 | } 130 | 131 | // getScreenState returns the current screen state 132 | func (p *NewScreen) getScreenState() string { 133 | p.mu.RLock() 134 | defer p.mu.RUnlock() 135 | return p.State 136 | } 137 | 138 | func setPlayPauseView(s string, screen *NewScreen) { 139 | screen.PlayPause.Enable() 140 | switch s { 141 | case "Play": 142 | screen.PlayPause.Text = "Play" 143 | screen.PlayPause.Icon = theme.MediaPlayIcon() 144 | screen.PlayPause.Refresh() 145 | case "Pause": 146 | screen.PlayPause.Text = "Pause" 147 | screen.PlayPause.Icon = theme.MediaPauseIcon() 148 | screen.PlayPause.Refresh() 149 | } 150 | } 151 | 152 | func setMuteUnmuteView(s string, screen *NewScreen) { 153 | switch s { 154 | case "Mute": 155 | screen.MuteUnmute.Icon = theme.VolumeMuteIcon() 156 | screen.MuteUnmute.Refresh() 157 | case "Unmute": 158 | screen.MuteUnmute.Icon = theme.VolumeUpIcon() 159 | screen.MuteUnmute.Refresh() 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /.github/workflows/发布软件.yml: -------------------------------------------------------------------------------- 1 | name: 发布软件 2 | 3 | on: 4 | push: # 代码推送到main分支自动触发工作流 5 | #branches: 6 | # - main 7 | paths: 8 | - '**.py' 9 | workflow_dispatch: # 手动触发 10 | 11 | permissions: write-all # 给所有工作写权限 12 | 13 | jobs: 14 | jobs_v: 15 | name: 构建版本号和变更信息 16 | runs-on: ubuntu-latest 17 | outputs: 18 | version: ${{ steps.create_version.outputs.tag_name }} # 版本号 19 | body: ${{ steps.create_version.outputs.body }} # 版本变更内容 20 | steps: 21 | - uses: release-drafter/release-drafter@v5 22 | id: create_version 23 | with: 24 | config-name: release-drafter.yml # 配置文件在 .github/release-drafter.yml 25 | disable-autolabeler: true # 禁止自动标签 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | - name: 查看变量 29 | run: | 30 | echo ${{ format('version={0}', steps.create_version.outputs.tag_name ) }} 31 | 32 | jobs_window: 33 | needs: jobs_v # 等待 jobs_v 任务完成才执行 34 | name: 构建window软件 35 | timeout-minutes: 5 36 | runs-on: windows-2022 37 | env: 38 | version: ${{ needs.jobs_v.outputs.version }} 39 | body: ${{ needs.jobs_v.outputs.body }} 40 | steps: 41 | - uses: actions/checkout@v3 42 | with: 43 | submodules: recursive 44 | - name: 读入环境信息 45 | run: | 46 | echo ${{ format('version {0}', env.version ) }} # 版本号 47 | - name: 编译环境设置 Python 3.9.13 48 | uses: actions/setup-python@v4 49 | with: 50 | python-version: "3.9.13" 51 | architecture: "x64" 52 | cache: 'pip' 53 | - name: 下载依赖文件 54 | run: | 55 | pip install -r requirements.txt 56 | - name: 编译exe 57 | run: | 58 | python run_write_version.py 59 | rm ./go2tv/go2tv 60 | pyinstaller --noconfirm --onefile --windowed --icon "app.ico" --add-data "nanodlna;nanodlna/" --add-data "go2tv;go2tv/" "easy_to_tv.py" 61 | 62 | - name: 测试运行情况 63 | uses: GuillaumeFalourd/assert-command-line-output@v2 64 | with: 65 | command_line: ./dist/easy_to_tv.exe test 66 | contains: app run success 67 | expected_result: PASSED 68 | - name: 上传产物 69 | uses: actions/upload-artifact@v3 70 | with: 71 | name: window 72 | path: ./dist/*.exe 73 | 74 | jobs_macos: 75 | needs: jobs_v 76 | name: 构建macos软件 77 | runs-on: macos-12 78 | env: 79 | version: ${{ needs.jobs_v.outputs.version }} 80 | body: ${{ needs.jobs_v.outputs.body }} 81 | steps: 82 | - uses: actions/checkout@v3 83 | with: 84 | submodules: recursive 85 | - name: 读入环境信息 86 | run: | 87 | echo ${{ format('version {0}', env.version ) }} 88 | - name: 编译环境设置 Python 3.9.13 89 | uses: actions/setup-python@v4 90 | with: 91 | python-version: "3.9.13" 92 | architecture: "x64" 93 | cache: 'pip' 94 | - name: 下载依赖文件 95 | run: | 96 | pip install -r requirements.txt 97 | - name: 编译 MacOS.app 98 | run: | 99 | python run_write_version.py 100 | rm ./go2tv/go2tv.exe 101 | pyinstaller --noconfirm --windowed --icon "app.icns" --add-data "nanodlna:nanodlna/" --add-data "go2tv:go2tv/" "easy_to_tv.py" 102 | 103 | - name: 测试运行情况 104 | uses: GuillaumeFalourd/assert-command-line-output@v2 105 | with: 106 | command_line: ./dist/easy_to_tv.app/Contents/MacOS/easy_to_tv test 107 | contains: app run success 108 | expected_result: PASSED 109 | - name: 创建压缩包 110 | run: | 111 | cd ./dist 112 | zip -r ./easy_to_tv_MacOS.zip ./easy_to_tv.app 113 | - name: 上传产物 114 | uses: actions/upload-artifact@v3 115 | with: 116 | name: macos 117 | path: ./dist/*.zip 118 | 119 | 120 | jobs4: 121 | needs: [ jobs_v,jobs_window,jobs_macos ] 122 | name: 发布版本 123 | runs-on: ubuntu-latest 124 | env: 125 | version: ${{ needs.jobs_v.outputs.version }} 126 | body: ${{ needs.jobs_v.outputs.body }} 127 | steps: 128 | - name: 下载产物 129 | id: download 130 | uses: actions/download-artifact@v3 131 | with: 132 | path: ./ 133 | - name: 读入环境信息 134 | run: | 135 | echo ${{ format('version {0}', env.version ) }} 136 | echo ${{steps.download.outputs.download-path}} 137 | ls -R 138 | 139 | - name: 发布文件 140 | uses: ncipollo/release-action@v1 141 | with: 142 | token: ${{ secrets.GITHUB_TOKEN }} 143 | allowUpdates: true # 覆盖文件 144 | #draft: true # 草稿 自己可见 版本号会保持一样 默认是自动发布 latest 145 | #prerelease: true # 预发布 别人可以看到 版本号会继续加 146 | tag: ${{ env.version }} # 版本号 v0.1.0 147 | body: ${{ env.body }} # 输出的内容 148 | artifacts: "window/*.exe,macos/*.zip" -------------------------------------------------------------------------------- /qtefun/组件/树形框.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | from PySide6.QtWidgets import QTreeWidgetItem 3 | 4 | from qtefun.组件.组件公共类 import 组件公共类 5 | 6 | 7 | # https://doc.qt.io/qt-6/qtreewidget.html 8 | # 树形框 9 | # 绑定事件 10 | # currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous) 当前项目已更改 11 | # itemActivated(QTreeWidgetItem *item, int column) 项目已激活 12 | # itemChanged(QTreeWidgetItem *item, int column) 项目已更改 13 | # itemClicked(QTreeWidgetItem *item, int column) 已单击项目 14 | # itemCollapsed(QTreeWidgetItem *item) 项目已折叠 15 | # itemDoubleClicked(QTreeWidgetItem *item, int column) 项目被双击 16 | # itemEntered(QTreeWidgetItem *item, int column) 已输入的项目 17 | # itemExpanded(QTreeWidgetItem *item) 项目已展开 18 | # itemPressed(QTreeWidgetItem *item, int column) 按下的项目 19 | # itemSelectionChanged() 选择项目已更改 20 | 21 | class 树形框节点类(QTreeWidgetItem): 22 | 23 | def 置标题(self, 第几列, 标题): 24 | self.setText(第几列, 标题) 25 | 26 | def 置标题多列(self, 标题列表, 从第几列开始=0): 27 | i = 从第几列开始 28 | for 标题 in 标题列表: 29 | self.setText(i, 标题) 30 | i = i + 1 31 | 32 | 33 | class 树形框(组件公共类): 34 | 对象 = None # type: QtWidgets.QTreeWidget 35 | 36 | # 设置列数 37 | def 设置列数(self, 列数): 38 | self.对象.setColumnCount(列数) 39 | 40 | # 设置表头 41 | def 设置表头(self, 表头=[]): 42 | self.对象.setHeaderLabels(表头) 43 | 44 | # 添加节点 45 | def 添加节点(self, 父节点=None, 第几列=None, 文本=None): 46 | if 父节点 is None: 47 | 父节点 = self.对象 48 | 节点 = 树形框节点类(父节点) 49 | if 第几列 is not None: 50 | 节点.setText(第几列, 文本) 51 | return 节点 52 | 53 | # 删除节点 54 | def 删除节点(self, 节点): 55 | 节点.parent().removeChild(节点) 56 | 57 | # 查询节点 58 | def 查询节点(self, 父节点=None, 第几列=0, 文本=None): 59 | if 父节点 is None: 60 | 父节点 = self.对象 61 | for i in range(父节点.childCount()): 62 | 节点 = 父节点.child(i) 63 | if 节点.text(第几列) == 文本: 64 | return 节点 # type : QTreeWidgetItem 65 | return None 66 | 67 | # 查询节点 68 | def 查询节点列表(self, 父节点=None, 文本列表=None): 69 | if 父节点 is None: 70 | 父节点 = self.对象 71 | 节点列表 = [] 72 | for i in range(父节点.childCount()): 73 | 节点 = 父节点.child(i) 74 | if 节点.text(0) in 文本列表: 75 | 节点列表.append(节点) 76 | return 节点列表 77 | 78 | # 选中节点 79 | def 选中节点(self, 节点): 80 | self.对象.setCurrentItem(节点) 81 | 82 | # 保证显示 83 | def 保证显示(self): 84 | # 把选择的项目显示出来 85 | self.对象.scrollToItem(self.对象.currentItem()) 86 | 87 | # 取项目数量 88 | def 取根节点数量(self): 89 | return self.对象.topLevelItemCount() 90 | 91 | def 取列数(self): 92 | return self.对象.columnCount() 93 | 94 | # 全部展开 95 | def 全部展开(self): 96 | self.对象.expandAll() 97 | 98 | # 获取和设置属性 现行选中项 99 | @property 100 | def 现行选中项(self): 101 | return self.对象.currentIndex() 102 | 103 | @现行选中项.setter 104 | def 现行选中项(self, 索引: int): 105 | return self.对象.setCurrentIndex(索引) 106 | 107 | # currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous) 当前项目已更改 108 | def 绑定事件当前项目已更改(self, 回调函数): 109 | """ 110 | 回调函数(当前选中:QTreeWidgetItem, 上一个:QTreeWidgetItem) 111 | """ 112 | self.对象.currentItemChanged.connect(回调函数) 113 | 114 | # itemActivated(QTreeWidgetItem *item, int column) 项目已激活 115 | def 绑定事件项目已激活(self, 回调函数): 116 | """ 117 | 回调函数(节点:QTreeWidgetItem, 列:int) 118 | """ 119 | self.对象.itemActivated.connect(回调函数) 120 | 121 | # itemChanged(QTreeWidgetItem *item, int column) 项目已更改 122 | def 绑定事件项目已更改(self, 回调函数): 123 | """ 124 | 回调函数(节点:QTreeWidgetItem, 列:int) 125 | """ 126 | self.对象.itemChanged.connect(回调函数) 127 | 128 | # itemCollapsed(QTreeWidgetItem *item) 项目已折叠 129 | def 绑定事件项目已折叠(self, 回调函数): 130 | """ 131 | 回调函数(节点:QTreeWidgetItem) 132 | """ 133 | self.对象.itemCollapsed.connect(回调函数) 134 | 135 | # itemExpanded(QTreeWidgetItem *item) 项目已展开 136 | def 绑定事件项目已展开(self, 回调函数): 137 | """ 138 | 回调函数(节点:QTreeWidgetItem) 139 | """ 140 | self.对象.itemExpanded.connect(回调函数) 141 | 142 | # itemPressed(QTreeWidgetItem *item, int column) 项目已按下 143 | def 绑定事件项目已按下(self, 回调函数): 144 | """ 145 | 回调函数(节点:QTreeWidgetItem, 列:int) 146 | """ 147 | self.对象.itemPressed.connect(回调函数) 148 | 149 | # itemSelectionChanged() 选择已更改 150 | def 绑定事件选择已更改(self, 回调函数): 151 | """ 152 | 回调函数() 153 | """ 154 | self.对象.itemSelectionChanged.connect(回调函数) 155 | 156 | # itemDoubleClicked(QTreeWidgetItem *item, int column) 项目被双击 157 | def 绑定事件项目被双击(self, 回调函数): 158 | """ 159 | 回调函数(节点:QTreeWidgetItem, 列:int) 160 | """ 161 | self.对象.itemDoubleClicked.connect(回调函数) 162 | 163 | # itemEntered(QTreeWidgetItem *item, int column) 已输入的项目 164 | def 绑定事件已输入的项目(self, 回调函数): 165 | """ 166 | 回调函数(节点:QTreeWidgetItem, 列:int) 167 | """ 168 | self.对象.itemEntered.connect(回调函数) 169 | 170 | # # itemClicked(QTreeWidgetItem *item, int column) 已单击项目 171 | def 绑定事件已单击项目(self, 回调函数): 172 | """ 173 | 回调函数(节点:QTreeWidgetItem, 列:int) 174 | """ 175 | self.对象.itemClicked.connect(回调函数) 176 | -------------------------------------------------------------------------------- /nanodlna/templates/metadata-example1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Amazing Spider-Man 2 5 | Unknown 6 | 2014-01-01 7 | Andrew Garfield 8 | Emma Stone 9 | Jamie Foxx 10 | Dane DeHaan 11 | Colm Feore 12 | Felicity Jones 13 | Paul Giamatti 14 | Sally Field 15 | Embeth Davidtz 16 | Campbell Scott 17 | Marton Csokas 18 | Louis Cancelmi 19 | Max Charles 20 | Sarah Gadon 21 | Stan Lee 22 | Chris Cooper 23 | Frank Deal 24 | Denis Leary 25 | Martin Sheen 26 | Chris Zylka 27 | B. J. Novak 28 | Alex Kurtzman 29 | Roberto Orci 30 | Jeff Pinkner 31 | Marc Webb 32 | Columbia Pictures 33 | Action 34 | Adventure 35 | Fantasy 36 | http://192.168.1.4:1318/%25/972ECCF3ECA468CE8D3B03508FB74541/sxCWu1t1pcBl7L5ofOJQEY1v8tp.jpg 37 | No more secrets. 38 | For Peter Parker, life is busy. Between taking out the bad guys as Spider-Man and spending time with the person he loves, Gwen Stacy, high school graduation cannot come quickly enough. Peter has not forgotten about the promise he made to Gwen’s father to protect her by staying away, but that is a promise he cannot keep. Things will change for Peter when a new villain, Electro, emerges, an old friend, Harry Osborn, returns, and Peter uncovers new clues about his past. 39 | Rated PG-13 40 | 617 41 | 2015-02-25T02:16:38+00:00 42 | 0 43 | 0 44 | http://192.168.1.4:1318/%25/62920C8391193FEA076FCD856B44BD45/The.Amazing.Spider.Man.2.2014.1080p.BluRay.x264.YIFY.mp4 45 | http://192.168.1.4:1318/%25/A3C9A0DF20A3900ED5659D6C487C3B95/tmFDgDmrdp5DYezwpL0ymQKIbnV.jpg 46 | http://192.168.1.4:1318/%25/972ECCF3ECA468CE8D3B03508FB74541/sxCWu1t1pcBl7L5ofOJQEY1v8tp.jpg 47 | http://192.168.1.4:1318/%25/0D6BB987C1E6EA3A283CEDFE57FD26A7/The.Amazing.Spider.Man.2.2014.1080p.BluRay.x264.YIFY.srt 48 | http://192.168.1.4:1318/%25/0D6BB987C1E6EA3A283CEDFE57FD26A7/The.Amazing.Spider.Man.2.2014.1080p.BluRay.x264.YIFY.srt 49 | http://192.168.1.4:1318/%25/0D6BB987C1E6EA3A283CEDFE57FD26A7/The.Amazing.Spider.Man.2.2014.1080p.BluRay.x264.YIFY.srt 50 | http://192.168.1.4:1318/%25/0D6BB987C1E6EA3A283CEDFE57FD26A7/The.Amazing.Spider.Man.2.2014.1080p.BluRay.x264.YIFY.srt 51 | 2014-08-10 52 | 6.9 53 | 248,581 54 | http://192.168.1.4:1318/%25/A3C9A0DF20A3900ED5659D6C487C3B95/tmFDgDmrdp5DYezwpL0ymQKIbnV.jpg 55 | http://192.168.1.4:1318/%25/972ECCF3ECA468CE8D3B03508FB74541/sxCWu1t1pcBl7L5ofOJQEY1v8tp.jpg 56 | tt1872181 57 | object.item.videoItem.movie 58 | 59 | 60 | -------------------------------------------------------------------------------- /nanodlna/devices.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: UTF-8 3 | 4 | import re 5 | import socket 6 | import struct 7 | import sys 8 | import xml.etree.ElementTree as ET 9 | 10 | if sys.version_info.major == 3: 11 | import urllib.request as urllibreq 12 | import urllib.parse as urllibparse 13 | else: 14 | import urllib2 as urllibreq 15 | import urlparse as urllibparse 16 | 17 | import logging 18 | import json 19 | 20 | SSDP_BROADCAST_PORT = 1900 21 | SSDP_BROADCAST_ADDR = "239.255.255.250" 22 | 23 | SSDP_BROADCAST_PARAMS = [ 24 | "M-SEARCH * HTTP/1.1", 25 | "HOST: {0}:{1}".format(SSDP_BROADCAST_ADDR, SSDP_BROADCAST_PORT), 26 | "MAN: \"ssdp:discover\"", "MX: 10", "ST: ssdp:all", "", ""] 27 | SSDP_BROADCAST_MSG = "\r\n".join(SSDP_BROADCAST_PARAMS) 28 | 29 | UPNP_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1" 30 | UPNP_SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1" 31 | 32 | 33 | def get_xml_field_text(xml_root, query): 34 | result = None 35 | if xml_root: 36 | node = xml_root.find(query) 37 | result = node.text if node is not None else None 38 | return result 39 | 40 | 41 | def register_device(location_url): 42 | 43 | xml_raw = urllibreq.urlopen(location_url).read().decode("UTF-8") 44 | # print( 45 | # "Device to be registered: {}".format( 46 | # json.dumps({ 47 | # "location_url": location_url, 48 | # "xml_raw": xml_raw 49 | # }) 50 | # ) 51 | # ) 52 | 53 | xml = re.sub(r"""\s(xmlns="[^"]+"|xmlns='[^']+')""", '', xml_raw, count=1) 54 | info = ET.fromstring(xml) 55 | 56 | location = urllibparse.urlparse(location_url) 57 | hostname = location.hostname 58 | 59 | device_root = info.find("./device") 60 | if not device_root: 61 | device_root = info.find( 62 | "./device/deviceList/device/" 63 | "[deviceType='{0}']".format( 64 | UPNP_DEVICE_TYPE 65 | ) 66 | ) 67 | 68 | friendly_name = get_xml_field_text(device_root, "./friendlyName") 69 | manufacturer = get_xml_field_text(device_root, "./manufacturer") 70 | action_url_path = get_xml_field_text( 71 | device_root, 72 | "./serviceList/service/" 73 | "[serviceType='{0}']/controlURL".format( 74 | UPNP_SERVICE_TYPE 75 | ) 76 | ) 77 | 78 | if action_url_path is not None: 79 | action_url = urllibparse.urljoin(location_url, action_url_path) 80 | else: 81 | action_url = None 82 | 83 | device = { 84 | "location": location_url, 85 | "hostname": hostname, 86 | "manufacturer": manufacturer, 87 | "friendly_name": friendly_name, 88 | "action_url": action_url, 89 | "st": UPNP_SERVICE_TYPE 90 | } 91 | 92 | # print( 93 | # "Device registered: {}".format( 94 | # json.dumps({ 95 | # "device_xml": xml, 96 | # "device_info": device 97 | # }) 98 | # ) 99 | # ) 100 | 101 | return device 102 | 103 | 104 | def remove_duplicates(devices): 105 | seen = set() 106 | result_devices = [] 107 | for device in devices: 108 | device_str = str(device) 109 | if device_str not in seen: 110 | result_devices.append(device) 111 | seen.add(device_str) 112 | return result_devices 113 | 114 | 115 | def get_devices(timeout=3.0, host=None): 116 | if not host: 117 | host = "0.0.0.0" 118 | # print("Searching for devices on {}".format(host)) 119 | 120 | # print("Configuring broadcast message") 121 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 122 | 123 | # OpenBSD needs the ttl for the IP_MULTICAST_TTL as an unsigned char 124 | ttl = struct.pack("B", 4) 125 | s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) 126 | 127 | s.bind((host, 0)) 128 | 129 | # print("Sending broadcast message") 130 | s.sendto(SSDP_BROADCAST_MSG.encode("UTF-8"), (SSDP_BROADCAST_ADDR, 131 | SSDP_BROADCAST_PORT)) 132 | 133 | # print("Waiting for devices ({} seconds)".format(timeout)) 134 | s.settimeout(timeout) 135 | 136 | devices = [] 137 | while True: 138 | 139 | try: 140 | data, addr = s.recvfrom(2048) 141 | except socket.timeout: 142 | break 143 | 144 | try: 145 | info = [a.split(":", 1) 146 | for a in data.decode("UTF-8").split("\r\n")[1:]] 147 | device = dict([(a[0].strip().lower(), a[1].strip()) 148 | for a in info if len(a) >= 2]) 149 | devices.append(device) 150 | # print( 151 | # "Device broadcast response: {}".format( 152 | # json.dumps({ 153 | # "broadcast_message_raw": data.decode("UTF-8"), 154 | # "broadcast_message_info": device 155 | # }) 156 | # ) 157 | # ) 158 | except Exception: 159 | pass 160 | 161 | devices_urls = [ 162 | dev["location"] 163 | for dev in devices 164 | if "st" in dev and 165 | "AVTransport" in dev["st"] 166 | ] 167 | 168 | devices = [ 169 | register_device(location_url) 170 | for location_url in devices_urls 171 | ] 172 | 173 | devices = remove_duplicates(devices) 174 | 175 | return devices 176 | 177 | 178 | if __name__ == "__main__": 179 | 180 | timeout = int(sys.argv[1]) if len(sys.argv) >= 2 else 5 181 | 182 | devices = get_devices(timeout, "0.0.0.0") 183 | 184 | for i, device in enumerate(devices, 1): 185 | print("Device {0}:\n{1}\n\n".format(i, json.dumps(device, indent=4))) 186 | -------------------------------------------------------------------------------- /qtefun/组件使用例子/5.树形框.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide6.QtWidgets import * 4 | 5 | from qtefun.组件.树形框 import 树形框 6 | 7 | 8 | class Main(QMainWindow): 9 | def __init__(self): 10 | super(Main, self).__init__() 11 | self.init_ui() 12 | 13 | def init_ui(self): 14 | self.resize(400, 400) 15 | self.setWindowTitle("树形框组件学习") 16 | self.show() 17 | 18 | self.树形框 = QTreeWidget(self) 19 | self.树形框.setGeometry(0, 0, 400, 300) 20 | self.树形框.show() 21 | self.树形框.setColumnCount(3) 22 | self.树形框.setHeaderLabels(['分类', "产品名称", '价格', '说明']) 23 | 24 | root = QTreeWidgetItem(self.树形框) 25 | root.setText(0, '产品') 26 | 27 | # 设置子节点1 28 | 电子产品 = QTreeWidgetItem(root) 29 | 电子产品.setText(0, '电子产品') 30 | 31 | 水果 = QTreeWidgetItem(root) 32 | 水果.setText(0, '水果') 33 | 34 | 书籍 = QTreeWidgetItem(root) 35 | 书籍.setText(0, '书籍') 36 | 37 | 电子产品子项 = QTreeWidgetItem(电子产品) 38 | 电子产品子项.setText(1, '苹果') 39 | 电子产品子项.setText(2, '999') 40 | 电子产品子项.setText(3, '真香') 41 | 电子产品子项 = QTreeWidgetItem(电子产品) 42 | 电子产品子项.setText(1, '华为') 43 | 电子产品子项.setText(2, '999') 44 | 电子产品子项.setText(3, '爱国') 45 | 电子产品子项 = QTreeWidgetItem(电子产品) 46 | 电子产品子项.setText(1, '小米') 47 | 电子产品子项.setText(2, '999') 48 | 电子产品子项.setText(3, '过保就坏') 49 | 50 | 水果子项 = QTreeWidgetItem(水果) 51 | 水果子项.setText(1, '苹果') 52 | 水果子项.setText(2, '999') 53 | 水果子项.setText(3, '好吃') 54 | 水果子项 = QTreeWidgetItem(水果) 55 | 水果子项.setText(1, '香蕉') 56 | 水果子项.setText(2, '999') 57 | 水果子项.setText(3, '好吃') 58 | 水果子项 = QTreeWidgetItem(水果) 59 | 水果子项.setText(1, '西瓜') 60 | 水果子项.setText(2, '999') 61 | 水果子项.setText(3, '好吃') 62 | 63 | 书籍子项 = QTreeWidgetItem(书籍) 64 | 书籍子项.setText(1, 'python入门') 65 | 书籍子项.setText(2, '999') 66 | 书籍子项.setText(3, '学习') 67 | 书籍子项 = QTreeWidgetItem(书籍) 68 | 书籍子项.setText(1, 'php入门') 69 | 书籍子项.setText(2, '999') 70 | 书籍子项.setText(3, '学习') 71 | 书籍子项 = QTreeWidgetItem(书籍) 72 | 书籍子项.setText(1, 'js入门') 73 | 书籍子项.setText(2, '999') 74 | 书籍子项.setText(3, '学习') 75 | 76 | self.树形框.expandAll() 77 | 78 | # self.树形框.clear() 79 | 80 | self.树形框1 = 树形框(self.树形框) 81 | self.树形框1.设置列数(3) 82 | self.树形框1.设置表头(['分类', "产品名称", '价格', '说明']) 83 | 根节点 = self.树形框1.添加节点(None, 0, '产品') 84 | 电子产品 = self.树形框1.添加节点(根节点, 0, '电子产品') 85 | 水果 = self.树形框1.添加节点(根节点, 0, '水果') 86 | self.书籍 = 书籍 = self.树形框1.添加节点(根节点, 0, '书籍') 87 | 88 | 水果子项 = self.树形框1.添加节点(水果, 0, '') 89 | 水果子项.置标题(1, '苹果') 90 | 水果子项.置标题(2, '999') 91 | 水果子项.置标题(3, '好吃') 92 | 93 | 书籍子项 = self.树形框1.添加节点(书籍) 94 | 书籍子项.置标题多列(['python入门', '999', '学习'], 1) 95 | 96 | 书籍子项 = self.树形框1.添加节点(书籍) 97 | 书籍子项.置标题多列(['php入门', '999', '学习'], 1) 98 | 99 | 书籍子项 = self.树形框1.添加节点(书籍) 100 | 书籍子项.置标题多列(['js入门', '999', '学习'], 1) 101 | 102 | self.树形框1.删除节点(书籍子项) 103 | # self.树形框1.删除节点(书籍) 104 | p = self.树形框1.查询节点(书籍, 1, "python入门") # type: QTreeWidgetItem 105 | print(p.text(1)) 106 | self.树形框1.删除节点(p) 107 | 108 | self.树形框1.全部展开() 109 | 110 | print("取根节点数量", self.树形框1.取根节点数量()) 111 | print("取列数", self.树形框1.取列数()) 112 | 113 | # 绑定事件 114 | # currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous) 当前项目已更改 115 | # itemActivated(QTreeWidgetItem *item, int column) 项目已激活 116 | # itemChanged(QTreeWidgetItem *item, int column) 项目已更改 117 | # itemClicked(QTreeWidgetItem *item, int column) 已单击项目 118 | # itemCollapsed(QTreeWidgetItem *item) 项目已折叠 119 | # itemDoubleClicked(QTreeWidgetItem *item, int column) 项目被双击 120 | # itemEntered(QTreeWidgetItem *item, int column) 已输入的项目 121 | # itemExpanded(QTreeWidgetItem *item) 项目已展开 122 | # itemPressed(QTreeWidgetItem *item, int column) 按下的项目 123 | # itemSelectionChanged() 选择项目已更改 124 | self.树形框1.绑定事件当前项目已更改(self.当前项目已更改) 125 | self.树形框1.绑定事件项目已激活(self.项目已激活) 126 | self.树形框1.绑定事件项目已更改(self.项目已更改) 127 | self.树形框1.绑定事件已单击项目(self.已单击项目) 128 | self.树形框1.绑定事件项目已折叠(self.项目已折叠) 129 | self.树形框1.绑定事件项目被双击(self.项目被双击) 130 | self.树形框1.绑定事件已输入的项目(self.项目已输入) 131 | self.树形框1.绑定事件项目已展开(self.项目已展开) 132 | self.树形框1.绑定事件项目已按下(self.项目按下) 133 | 134 | # 创建按钮 135 | self.按钮 = QPushButton(self) 136 | self.按钮.clicked.connect(self.按钮点击) 137 | self.按钮.move(0, 300) 138 | self.按钮.resize(100, 40) 139 | self.按钮.setText('查找') 140 | self.按钮.show() 141 | 142 | def 按钮点击(self): 143 | p = self.树形框1.查询节点(self.书籍, 1, "php入门") # type: QTreeWidgetItem 144 | print(p.text(1)) 145 | self.树形框1.选中节点(p) 146 | self.树形框1.保证显示() 147 | 148 | def 当前项目已更改(self, 当前选中: QTreeWidgetItem, 上一个: QTreeWidgetItem): 149 | print("当前项目已更改", 当前选中.text(1)) 150 | 151 | def 项目已激活(self, 选中项目: QTreeWidgetItem, 列: int): 152 | print("项目已激活", 选中项目.text(1)) 153 | 154 | def 项目已更改(self, 选中项目: QTreeWidgetItem, 列: int): 155 | print("项目已更改", 选中项目.text(1)) 156 | 157 | def 已单击项目(self, 选中项目: QTreeWidgetItem, 列: int): 158 | print("已单击项目", 选中项目.text(1)) 159 | 160 | def 项目已折叠(self, 选中项目: QTreeWidgetItem): 161 | print("项目已折叠", 选中项目.text(1)) 162 | 163 | def 项目被双击(self, 选中项目: QTreeWidgetItem, 列: int): 164 | print("项目被双击", 选中项目.text(1)) 165 | 166 | def 项目已输入(self, 选中项目: QTreeWidgetItem, 列: int): 167 | print("项目已输入", 选中项目.text(1)) 168 | 169 | def 项目已展开(self, 选中项目: QTreeWidgetItem): 170 | print("项目已展开", 选中项目.text(1)) 171 | 172 | def 项目按下(self, 选中项目: QTreeWidgetItem, 列: int): 173 | print("项目按下", 选中项目.text(1),self.树形框1.现行选中项.column(),self.树形框1.现行选中项.row()) 174 | 175 | app = QApplication([]) 176 | # 创建窗口 400x400 177 | win = Main() 178 | 179 | sys.exit(app.exec()) 180 | -------------------------------------------------------------------------------- /nanodlna/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import print_function 4 | 5 | import argparse 6 | import json 7 | import os 8 | import sys 9 | import signal 10 | import datetime 11 | import tempfile 12 | 13 | from . import devices, dlna, streaming 14 | 15 | import logging 16 | 17 | 18 | def set_logs(args): 19 | 20 | log_filename = os.path.join( 21 | tempfile.mkdtemp(), 22 | "nanodlna-{}.log".format( 23 | datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S") 24 | ) 25 | ) 26 | 27 | logging.basicConfig( 28 | filename=log_filename, 29 | filemode="w", 30 | level=logging.INFO, 31 | format="[ %(asctime)s ] %(levelname)s : %(message)s" 32 | ) 33 | 34 | if args.debug_activated: 35 | logging.getLogger().setLevel(logging.DEBUG) 36 | 37 | print("nano-dlna log will be saved here: {}".format(log_filename)) 38 | 39 | 40 | def get_subtitle(file_video): 41 | 42 | video, extension = os.path.splitext(file_video) 43 | 44 | file_subtitle = "{0}.srt".format(video) 45 | 46 | if not os.path.exists(file_subtitle): 47 | return None 48 | return file_subtitle 49 | 50 | 51 | def list_devices(args): 52 | 53 | # set_logs(args) 54 | 55 | logging.info("Scanning devices...") 56 | my_devices = devices.get_devices(args.timeout, args.local_host) 57 | logging.info("Number of devices found: {}".format(len(my_devices))) 58 | 59 | for i, device in enumerate(my_devices, 1): 60 | print("Device {0}:\n{1}\n\n".format(i, json.dumps(device, indent=4))) 61 | 62 | 63 | def find_device(args): 64 | 65 | logging.info("Selecting device to play") 66 | 67 | device = None 68 | 69 | if args.device_url: 70 | logging.info("Select device by URL") 71 | device = devices.register_device(args.device_url) 72 | else: 73 | my_devices = devices.get_devices(args.timeout, args.local_host) 74 | 75 | if len(my_devices) > 0: 76 | if args.device_query: 77 | logging.info("Select device by query") 78 | device = [ 79 | device for device in my_devices 80 | if args.device_query.lower() in str(device).lower()][0] 81 | else: 82 | logging.info("Select first device") 83 | device = my_devices[0] 84 | 85 | return device 86 | 87 | 88 | def play(args): 89 | 90 | set_logs(args) 91 | 92 | logging.info("Starting to play") 93 | 94 | # Get video and subtitle file names 95 | 96 | files = {"file_video": args.file_video} 97 | 98 | if args.use_subtitle: 99 | 100 | if not args.file_subtitle: 101 | args.file_subtitle = get_subtitle(args.file_video) 102 | 103 | if args.file_subtitle: 104 | files["file_subtitle"] = args.file_subtitle 105 | 106 | logging.info("Media files: {}".format(json.dumps(files))) 107 | 108 | device = find_device(args) 109 | if not device: 110 | sys.exit("No devices found.") 111 | 112 | logging.info("Device selected: {}".format(json.dumps(device))) 113 | 114 | # Configure streaming server 115 | logging.info("Configuring streaming server") 116 | 117 | target_ip = device["hostname"] 118 | if args.local_host: 119 | serve_ip = args.local_host 120 | else: 121 | serve_ip = streaming.get_serve_ip(target_ip) 122 | files_urls = streaming.start_server(files, serve_ip) 123 | 124 | logging.info("Streaming server ready") 125 | 126 | # Register handler if interrupt signal is received 127 | signal.signal(signal.SIGINT, build_handler_stop(device)) 128 | 129 | # Play the video through DLNA protocol 130 | logging.info("Sending play command") 131 | dlna.play(files_urls, device) 132 | 133 | 134 | def build_handler_stop(device): 135 | def signal_handler(sig, frame): 136 | 137 | logging.info("Interrupt signal detected") 138 | 139 | logging.info("Sending stop command to render device") 140 | dlna.stop(device) 141 | 142 | logging.info("Stopping streaming server") 143 | streaming.stop_server() 144 | 145 | sys.exit( 146 | "Interrupt signal detected. " 147 | "Sent stop command to render device and " 148 | "stopped streaming. " 149 | "nano-dlna will exit now!" 150 | ) 151 | return signal_handler 152 | 153 | 154 | def pause(args): 155 | 156 | set_logs(args) 157 | 158 | logging.info("Selecting device to pause") 159 | device = find_device(args) 160 | 161 | # Pause through DLNA protocol 162 | logging.info("Sending pause command") 163 | dlna.pause(device) 164 | 165 | 166 | def stop(args): 167 | 168 | set_logs(args) 169 | 170 | logging.info("Selecting device to stop") 171 | device = find_device(args) 172 | 173 | # Stop through DLNA protocol 174 | logging.info("Sending stop command") 175 | dlna.stop(device) 176 | 177 | 178 | def run(): 179 | 180 | parser = argparse.ArgumentParser( 181 | description="A minimal UPnP/DLNA media streamer.") 182 | parser.set_defaults(func=lambda args: parser.print_help()) 183 | parser.add_argument("-H", "--host", dest="local_host") 184 | parser.add_argument("-t", "--timeout", type=float, default=5) 185 | parser.add_argument("-b", "--debug", 186 | dest="debug_activated", action="store_true") 187 | subparsers = parser.add_subparsers(dest="subparser_name") 188 | 189 | p_list = subparsers.add_parser('list') 190 | p_list.set_defaults(func=list_devices) 191 | 192 | p_play = subparsers.add_parser('play') 193 | p_play.add_argument("-d", "--device", dest="device_url") 194 | p_play.add_argument("-q", "--query-device", dest="device_query") 195 | p_play.add_argument("-s", "--subtitle", dest="file_subtitle") 196 | p_play.add_argument("-n", "--no-subtitle", 197 | dest="use_subtitle", action="store_false") 198 | p_play.add_argument("file_video") 199 | p_play.set_defaults(func=play) 200 | 201 | p_pause = subparsers.add_parser('pause') 202 | p_pause.add_argument("-d", "--device", dest="device_url") 203 | p_pause.add_argument("-q", "--query-device", dest="device_query") 204 | p_pause.set_defaults(func=pause) 205 | 206 | p_stop = subparsers.add_parser('stop') 207 | p_stop.add_argument("-d", "--device", dest="device_url") 208 | p_stop.add_argument("-q", "--query-device", dest="device_query") 209 | p_stop.set_defaults(func=stop) 210 | 211 | args = parser.parse_args() 212 | 213 | args.func(args) 214 | 215 | 216 | if __name__ == "__main__": 217 | 218 | run() 219 | -------------------------------------------------------------------------------- /go2tv_res/internal/gui/gui.go: -------------------------------------------------------------------------------- 1 | //go:build !(android || ios) 2 | // +build !android,!ios 3 | 4 | package gui 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | 12 | "fyne.io/fyne/v2" 13 | "fyne.io/fyne/v2/app" 14 | "fyne.io/fyne/v2/container" 15 | "fyne.io/fyne/v2/dialog" 16 | "fyne.io/fyne/v2/theme" 17 | "fyne.io/fyne/v2/widget" 18 | "github.com/alexballas/go2tv/httphandlers" 19 | "github.com/alexballas/go2tv/soapcalls" 20 | "github.com/pkg/errors" 21 | ) 22 | 23 | // NewScreen . 24 | type NewScreen struct { 25 | mu sync.RWMutex 26 | Current fyne.Window 27 | ExternalMediaURL *widget.Check 28 | Stop *widget.Button 29 | MuteUnmute *widget.Button 30 | CheckVersion *widget.Button 31 | tvdata *soapcalls.TVPayload 32 | CustomSubsCheck *widget.Check 33 | MediaText *widget.Entry 34 | SubsText *widget.Entry 35 | DeviceList *widget.List 36 | httpserver *httphandlers.HTTPserver 37 | PlayPause *widget.Button 38 | selectedDevice devType 39 | State string 40 | controlURL string 41 | eventlURL string 42 | renderingControlURL string 43 | currentmfolder string 44 | mediafile string 45 | subsfile string 46 | version string 47 | mediaFormats []string 48 | NextMedia bool 49 | Medialoop bool 50 | Transcode bool 51 | } 52 | 53 | type devType struct { 54 | name string 55 | addr string 56 | } 57 | 58 | type mainButtonsLayout struct { 59 | buttonHeight float32 60 | } 61 | 62 | // Start . 63 | func Start(s *NewScreen) { 64 | w := s.Current 65 | 66 | tabs := container.NewAppTabs( 67 | container.NewTabItem("Go2TV", container.NewPadded(mainWindow(s))), 68 | container.NewTabItem("Settings", container.NewPadded(settingsWindow(s))), 69 | container.NewTabItem("About", aboutWindow(s)), 70 | ) 71 | 72 | tabs.OnSelected = func(t *container.TabItem) { 73 | t.Content.Refresh() 74 | } 75 | 76 | w.SetContent(tabs) 77 | w.Resize(fyne.NewSize(w.Canvas().Size().Width, w.Canvas().Size().Height*1.3)) 78 | w.CenterOnScreen() 79 | w.SetMaster() 80 | w.ShowAndRun() 81 | os.Exit(0) 82 | } 83 | 84 | // EmitMsg Method to implement the screen interface 85 | func (p *NewScreen) EmitMsg(a string) { 86 | switch a { 87 | case "Playing": 88 | setPlayPauseView("Pause", p) 89 | p.updateScreenState("Playing") 90 | case "Paused": 91 | setPlayPauseView("Play", p) 92 | p.updateScreenState("Paused") 93 | case "Stopped": 94 | setPlayPauseView("Play", p) 95 | p.updateScreenState("Stopped") 96 | stopAction(p) 97 | default: 98 | dialog.ShowInformation("?", "Unknown callback value", p.Current) 99 | } 100 | } 101 | 102 | // Fini Method to implement the screen interface. 103 | // Will only be executed when we receive a callback message, 104 | // not when we explicitly click the Stop button. 105 | func (p *NewScreen) Fini() { 106 | if p.NextMedia { 107 | selectNextMedia(p) 108 | } 109 | // Main media loop logic 110 | if p.Medialoop { 111 | playAction(p) 112 | } 113 | } 114 | 115 | // InitFyneNewScreen . 116 | func InitFyneNewScreen(v string) *NewScreen { 117 | go2tv := app.NewWithID("com.alexballas.go2tv") 118 | w := go2tv.NewWindow("Go2TV") 119 | currentdir, err := os.Getwd() 120 | if err != nil { 121 | currentdir = "" 122 | } 123 | 124 | theme := fyne.CurrentApp().Preferences().StringWithFallback("Theme", "Default") 125 | fyne.CurrentApp().Settings().SetTheme(go2tvTheme{theme}) 126 | 127 | return &NewScreen{ 128 | Current: w, 129 | currentmfolder: currentdir, 130 | mediaFormats: []string{".mp4", ".avi", ".mkv", ".mpeg", ".mov", ".webm", ".m4v", ".mpv", ".mp3", ".flac", ".wav", ".jpg", ".jpeg", ".png"}, 131 | version: v, 132 | } 133 | } 134 | 135 | func check(win fyne.Window, err error) { 136 | if err != nil { 137 | cleanErr := strings.ReplaceAll(err.Error(), ": ", "\n") 138 | dialog.ShowError(errors.New(cleanErr), win) 139 | } 140 | } 141 | 142 | func selectNextMedia(screen *NewScreen) { 143 | w := screen.Current 144 | filedir := filepath.Dir(screen.mediafile) 145 | filelist, err := os.ReadDir(filedir) 146 | check(w, err) 147 | 148 | var breaknext bool 149 | var n int 150 | var totalMedia int 151 | var firstMedia string 152 | 153 | for _, f := range filelist { 154 | isMedia := false 155 | for _, vext := range screen.mediaFormats { 156 | if filepath.Ext(filepath.Join(filedir, f.Name())) == vext { 157 | 158 | if firstMedia == "" { 159 | firstMedia = f.Name() 160 | } 161 | 162 | isMedia = true 163 | break 164 | } 165 | } 166 | 167 | if !isMedia { 168 | continue 169 | } 170 | 171 | totalMedia += 1 172 | } 173 | 174 | for _, f := range filelist { 175 | isMedia := false 176 | for _, vext := range screen.mediaFormats { 177 | if filepath.Ext(filepath.Join(filedir, f.Name())) == vext { 178 | isMedia = true 179 | break 180 | } 181 | } 182 | 183 | if !isMedia { 184 | continue 185 | } 186 | 187 | n += 1 188 | 189 | if f.Name() == filepath.Base(screen.mediafile) { 190 | if totalMedia == n { 191 | // start over 192 | screen.MediaText.Text = firstMedia 193 | screen.mediafile = filepath.Join(filedir, firstMedia) 194 | screen.MediaText.Refresh() 195 | } 196 | 197 | breaknext = true 198 | continue 199 | } 200 | 201 | if breaknext { 202 | screen.MediaText.Text = f.Name() 203 | screen.mediafile = filepath.Join(filedir, f.Name()) 204 | screen.MediaText.Refresh() 205 | 206 | if !screen.CustomSubsCheck.Checked { 207 | selectSubs(screen.mediafile, screen) 208 | } 209 | break 210 | } 211 | } 212 | } 213 | 214 | func selectSubs(v string, screen *NewScreen) { 215 | possibleSub := v[0:len(v)- 216 | len(filepath.Ext(v))] + ".srt" 217 | 218 | screen.SubsText.Text = filepath.Base(possibleSub) 219 | screen.subsfile = possibleSub 220 | 221 | if _, err := os.Stat(possibleSub); os.IsNotExist(err) { 222 | screen.SubsText.Text = "" 223 | screen.subsfile = "" 224 | } 225 | 226 | screen.SubsText.Refresh() 227 | } 228 | 229 | func setPlayPauseView(s string, screen *NewScreen) { 230 | screen.PlayPause.Enable() 231 | switch s { 232 | case "Play": 233 | screen.PlayPause.Text = "Play" 234 | screen.PlayPause.Icon = theme.MediaPlayIcon() 235 | screen.PlayPause.Refresh() 236 | case "Pause": 237 | screen.PlayPause.Text = "Pause" 238 | screen.PlayPause.Icon = theme.MediaPauseIcon() 239 | screen.PlayPause.Refresh() 240 | } 241 | } 242 | 243 | func setMuteUnmuteView(s string, screen *NewScreen) { 244 | switch s { 245 | case "Mute": 246 | screen.MuteUnmute.Icon = theme.VolumeMuteIcon() 247 | screen.MuteUnmute.Refresh() 248 | case "Unmute": 249 | screen.MuteUnmute.Icon = theme.VolumeUpIcon() 250 | screen.MuteUnmute.Refresh() 251 | } 252 | } 253 | 254 | // updateScreenState updates the screen state based on 255 | // the emitted messages. The State variable is used across 256 | // the GUI interface to control certain flows. 257 | func (p *NewScreen) updateScreenState(a string) { 258 | p.mu.Lock() 259 | p.State = a 260 | p.mu.Unlock() 261 | } 262 | 263 | // getScreenState returns the current screen state 264 | func (p *NewScreen) getScreenState() string { 265 | p.mu.RLock() 266 | defer p.mu.RUnlock() 267 | return p.State 268 | } 269 | -------------------------------------------------------------------------------- /ui_多多投屏.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | duolabmeng6 4 | MainWindow 5 | 6 | 7 | 8 | 0 9 | 0 10 | 397 11 | 265 12 | 13 | 14 | 15 | 多多投屏 16 | 17 | 18 | 19 | :/app/app.png:/app/app.png 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 0 30 | 0 31 | 32 | 33 | 34 | 35 | 0 36 | 32 37 | 38 | 39 | 40 | 选择文件 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 0 49 | 0 50 | 51 | 52 | 53 | 54 | 16777215 55 | 30 56 | 57 | 58 | 59 | 设备列表 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 0 68 | 0 69 | 70 | 71 | 72 | 73 | 0 74 | 32 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 0 84 | 0 85 | 86 | 87 | 88 | 89 | 0 90 | 32 91 | 92 | 93 | 94 | 刷新 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 0 103 | 0 104 | 105 | 106 | 107 | 108 | 16777215 109 | 24 110 | 111 | 112 | 113 | 投屏的文件路径 / Url 地址 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 1 122 | 0 123 | 124 | 125 | 126 | 127 | 0 128 | 32 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 拖放文件后直接开始播放 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 0 149 | 0 150 | 151 | 152 | 153 | 154 | 0 155 | 32 156 | 157 | 158 | 159 | 检查更新 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 0 168 | 0 169 | 170 | 171 | 172 | 173 | 0 174 | 32 175 | 176 | 177 | 178 | 开始播放 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 0 187 | 0 188 | 189 | 190 | 191 | 192 | 0 193 | 32 194 | 195 | 196 | 197 | 停止播放 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 0 209 | 0 210 | 397 211 | 24 212 | 213 | 214 | 215 | 216 | 217 | 218 | https://hub.fastgit.xyz/duolabmeng6/easy_to_tv 219 | 220 | 221 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /qtefun/组件/组件公共类.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | from PySide6.QtCore import Qt 3 | from PySide6.QtGui import QFont, QPalette, QColor, QPixmap, QIcon 4 | 5 | from qtefun.组件.组件汉化基类 import 组件汉化基类 6 | 7 | 8 | class 组件公共类(组件汉化基类): 9 | """ 10 | 配置组件方法和属性 11 | 对象名称 12 | 13 | 标题 14 | 宽度 15 | 高度 16 | 顶边 17 | 左边 18 | 右边 19 | 底边 20 | 大小 21 | 位置 22 | 可视 23 | 移动 24 | 背景颜色 25 | 背景图片 26 | 字体颜色 27 | 鼠标样式 28 | 禁用 29 | 可选 配合选中属性使用 30 | 选中 31 | 图标 32 | 图标大小 33 | 提示文本 34 | 状态栏文本 35 | 状态栏图标 36 | 状态栏图标大小 37 | 最大宽度 38 | 最大高度 39 | 最小宽度 40 | 最小高度 41 | 42 | 43 | 44 | 45 | 46 | """ 47 | 对象 = None # type: QtWidgets.QWidget 48 | 49 | # 获取对象名称 50 | @property 51 | def 名称(self): 52 | return self.对象.getObjectName() 53 | 54 | # 设置对象名称 55 | @名称.setter 56 | def 名称(self, value: str): 57 | return self.对象.setObjectName(value) 58 | 59 | 60 | # 获取窗口宽度 61 | @property 62 | def 宽度(self): 63 | return self.对象.width() 64 | 65 | # 设置窗口宽度 66 | @宽度.setter 67 | def 宽度(self, value: int): 68 | return self.对象.setFixedWidth(value) 69 | 70 | # 获取窗口高度 71 | @property 72 | def 高度(self): 73 | return self.对象.height() 74 | 75 | # 设置窗口高度 76 | @高度.setter 77 | def 高度(self, value: int): 78 | return self.对象.setFixedHeight(value) 79 | 80 | # 获取窗口左边 81 | @property 82 | def 左边(self): 83 | return self.对象.x() 84 | 85 | # 设置窗口左边 86 | @左边.setter 87 | def 左边(self, value: int): 88 | return self.对象.move(value, self.对象.y()) 89 | 90 | # 获取窗口上边 91 | @property 92 | def 顶边(self): 93 | return self.对象.y() 94 | 95 | # 设置窗口顶边 96 | @顶边.setter 97 | def 顶边(self, value: int): 98 | return self.对象.move(self.对象.x(), value) 99 | 100 | # 获取窗口右边 101 | @property 102 | def 右边(self): 103 | return self.对象.x() + self.对象.width() 104 | 105 | # 设置窗口右边 106 | @右边.setter 107 | def 右边(self, value: int): 108 | return self.对象.move(value - self.对象.width(), self.对象.y()) 109 | 110 | # 获取窗口底边 111 | @property 112 | def 底边(self): 113 | return self.对象.y() + self.对象.height() 114 | 115 | # 设置窗口底边 116 | @底边.setter 117 | def 底边(self, value: int): 118 | return self.对象.move(self.对象.x(), value - self.对象.height()) 119 | 120 | # 获取窗口大小 121 | @property 122 | def 大小(self): 123 | return self.对象.size() 124 | 125 | # 设置窗口大小 126 | @大小.setter 127 | def 大小(self, value: tuple): 128 | return self.对象.setFixedSize(value) 129 | 130 | # 获取窗口位置 131 | @property 132 | def 位置(self): 133 | return self.对象.pos() 134 | 135 | # 设置窗口位置 136 | @位置.setter 137 | def 位置(self, value: tuple): 138 | return self.对象.move(value) 139 | 140 | # 获取窗口是否可见 141 | @property 142 | def 可视(self): 143 | return self.对象.isVisible() 144 | 145 | # 设置窗口是否可见 146 | @可视.setter 147 | def 可视(self, value: bool): 148 | return self.对象.setVisible(value) 149 | 150 | # 移动 151 | def 移动(self, x: int, y: int): 152 | return self.对象.move(x, y) 153 | 154 | # 获取背景颜色 155 | @property 156 | def 背景颜色(self): 157 | return self.对象.palette().color(QPalette.Background) 158 | 159 | # 设置背景颜色 160 | @背景颜色.setter 161 | def 背景颜色(self, value: QColor): 162 | return self.对象.setPalette(QPalette(value)) 163 | # 设置背景图片 164 | @property 165 | def 背景图片(self): 166 | return self.对象.background() 167 | 168 | # 设置背景图片 169 | @背景图片.setter 170 | def 背景图片(self, value: QPixmap): 171 | return self.对象.setBackground(value) 172 | 173 | # 获取字体 174 | @property 175 | def 字体(self): 176 | return self.对象.font() 177 | 178 | # 设置字体 179 | @字体.setter 180 | def 字体(self, value: QFont): 181 | return self.对象.setFont(value) 182 | 183 | # 获取字体大小 184 | @property 185 | def 字体大小(self): 186 | return self.对象.font().pointSize() 187 | 188 | # 设置字体大小 189 | @字体大小.setter 190 | def 字体大小(self, value: int): 191 | return self.对象.setFont(QFont(self.对象.font().family(), value)) 192 | 193 | 194 | # 获取鼠标样式 195 | @property 196 | def 鼠标样式(self): 197 | return self.对象.cursor() 198 | 199 | @鼠标样式.setter 200 | def 鼠标样式(self, 样式: Qt.CursorShape): 201 | ''' 202 | https://doc.qt.io/qtforpython/PySide6/QtCore/Qt.html?highlight=cursorshape#PySide6.QtCore.PySide6.QtCore.Qt.CursorShape 203 | ''' 204 | return self.对象.setCursor(样式) 205 | 206 | # 获取提示文本 207 | @property 208 | def 提示文本(self): 209 | return self.对象.toolTip() 210 | 211 | # 设置提示文本 212 | @提示文本.setter 213 | def 提示文本(self, value: str): 214 | return self.对象.setToolTip(value) 215 | 216 | # 获取标题 217 | @property 218 | def 标题(self): 219 | return self.对象.windowTitle() 220 | 221 | # 设置标题 222 | @标题.setter 223 | def 标题(self, value: str): 224 | return self.对象.setWindowTitle(value) 225 | 226 | # 获取状态栏文本 227 | @property 228 | def 状态栏文本(self): 229 | return self.对象.statusTip() 230 | 231 | # 设置状态栏文本 232 | @状态栏文本.setter 233 | def 状态栏文本(self, value: str): 234 | return self.对象.setStatusTip(value) 235 | 236 | # 获取状态栏图标 237 | @property 238 | def 状态栏图标(self): 239 | return self.对象.windowIcon() 240 | 241 | # 设置状态栏图标 242 | @状态栏图标.setter 243 | def 状态栏图标(self, value: QIcon): 244 | return self.对象.setWindowIcon(value) 245 | 246 | @property 247 | def 禁用(self): 248 | return not self.对象.isEnabled() 249 | 250 | @禁用.setter 251 | def 禁用(self, value): 252 | return self.对象.setEnabled(not value) 253 | 254 | # 检查是否可选 255 | @property 256 | def 可选(self): 257 | return self.对象.isCheckable() 258 | 259 | # 设置是否可选 260 | @可选.setter 261 | def 可选(self, value: bool): 262 | return self.对象.setCheckable(value) 263 | 264 | @property 265 | def 选中(self): 266 | return self.对象.isChecked() 267 | 268 | @选中.setter 269 | def 选中(self, value): 270 | return self.对象.setChecked(value) 271 | 272 | @property 273 | def 图标(self): 274 | return self.对象.icon() 275 | 276 | @图标.setter 277 | def 图标(self, value): 278 | return self.对象.setIcon(value) 279 | 280 | # 设置图标大小 281 | @property 282 | def 图标大小(self): 283 | return self.对象.iconSize() 284 | 285 | @图标大小.setter 286 | def 图标大小(self, value): 287 | return self.对象.setIconSize(value) 288 | 289 | # 获取 最大宽度 290 | @property 291 | def 最大宽度(self): 292 | return self.对象.maximumWidth() 293 | 294 | # 设置 最大宽度 295 | @最大宽度.setter 296 | def 最大宽度(self, value): 297 | return self.对象.setMaximumWidth(value) 298 | 299 | # 获取 最大高度 300 | @property 301 | def 最大高度(self): 302 | return self.对象.maximumHeight() 303 | 304 | # 设置 最大高度 305 | @最大高度.setter 306 | def 最大高度(self, value): 307 | return self.对象.setMaximumHeight(value) 308 | 309 | # 获取 最小宽度 310 | @property 311 | def 最小宽度(self): 312 | return self.对象.minimumWidth() 313 | 314 | # 设置 最小宽度 315 | @最小宽度.setter 316 | def 最小宽度(self, value): 317 | return self.对象.setMinimumWidth(value) 318 | 319 | # 获取 最小高度 320 | @property 321 | def 最小高度(self): 322 | return self.对象.minimumHeight() 323 | 324 | # 设置 最小高度 325 | @最小高度.setter 326 | def 最小高度(self, value): 327 | return self.对象.setMinimumHeight(value) 328 | -------------------------------------------------------------------------------- /qtefun/README.md: -------------------------------------------------------------------------------- 1 | # qtefun qt版易函数 2 | 3 | 中文函数简单易用~ 4 | 5 | # 使用方法 6 | 7 | ## 主窗口 引入 qtefun.部件公共类 8 | 9 | 该类继承 QtWidgets.QWidget 提供了窗口操作的中文函数 10 | 11 | ```python 12 | import qtefun 13 | 14 | 15 | class myApp(qtefun.部件公共类): 16 | ``` 17 | 18 | # 组件中文化 19 | 20 | 将ui对象包装一下即可实现中文化 不会丢失原有的对象的功能 21 | 22 | 例如 `self.ui.textEdit.对象.setText("中文") ` 23 | 24 | 这种写法 `对象.` ide会有提示 `对象` 为 `QtWidgets.QPushButton` 的类 25 | 26 | 也可以改为 `self.ui.textEdit.setText("中文") ` 27 | 28 | ```python 29 | import qtefun 30 | from qtefun.组件.富文本编辑框 import 富文本编辑框 31 | from qtefun.组件.按钮 import 按钮 32 | from qtefun.公共函数 import 异常检测, 加载ui文件 33 | 34 | 35 | @异常检测 36 | class myApp(qtefun.部件公共类): 37 | def __init__(self): 38 | super().__init__() 39 | 40 | self.ui = 加载ui文件("app.ui", self) 41 | self.ui.show() 42 | 43 | self.标题 = "祖国 您好~" 44 | self.ui.pushButton = 按钮(self.ui.pushButton) 45 | self.ui.textEdit = 富文本编辑框(self.ui.textEdit) 46 | 47 | self.ui.pushButton.标题 = "祖国 您好~" 48 | self.ui.pushButton.宽度 = 148 49 | self.ui.pushButton.高度 = 48 50 | self.ui.pushButton.图标 = qtawesome.icon("fa5s.flag", color="red") 51 | self.ui.pushButton.图标大小 = QtCore.QSize(24, 24) 52 | self.ui.pushButton.绑定事件_按下(self.按钮被点击) 53 | 54 | self.ui.textEdit.内容 = "祖国 您好~" 55 | self.ui.textEdit.绑定事件_内容被改变(self.内容被改变) 56 | 57 | def 按钮被点击(self): 58 | pass 59 | print("祖国 您好~") 60 | 61 | def 内容被改变(self): 62 | print("内容被改变", self.ui.textEdit.内容) 63 | ``` 64 | 65 | # 组件对应的英文 66 | 67 | | 英文 | 中文 | 68 | | ---- |----------| 69 | | `QtWidgets.QPushButton` | `按钮` | 70 | | `QtWidgets.QLineEdit` | `单行文本框` | 71 | | `QtWidgets.QPlainTextEdit` | `纯文本编辑框` | 72 | | `QtWidgets.QTextEdit` | `富文本编辑框` | 73 | 74 | # 窗口通讯 75 | 76 | 引入简化版的 消息通信 类 就可以实现窗口间的通讯 77 | 78 | 79 | ```python 80 | from qtefun.消息通信 import 消息通信 81 | ``` 82 | 83 | ## 主窗口 main.py 定义 84 | 85 | ```python 86 | 主窗口信号 = 消息通信(str) 87 | ``` 88 | 89 | ### 接收消息 90 | 91 | ```python 92 | self.主窗口信号.接收消息(self.win_login.父窗口消息) 93 | self.win_login.子窗口信号.接收消息(self.子窗口消息) 94 | ``` 95 | 96 | ### 定义接收函数 97 | 98 | ```python 99 | def 子窗口消息(self, 消息内容): 100 | print("子窗口消息", 消息内容) 101 | ``` 102 | 103 | ### 发送消息 104 | 105 | ```python 106 | self.主窗口信号.发送消息("发送给子窗口") 107 | ``` 108 | 109 | ## 子窗口 win_login.py 定义 110 | 111 | ```python 112 | 子窗口信号 = 消息通信(str) 113 | ``` 114 | 115 | ### 定义接收函数 116 | 117 | ```python 118 | def 父窗口消息(self, 消息内容): 119 | print("父窗口消息",消息内容) 120 | ``` 121 | 122 | ### 发送消息 123 | 124 | ```python 125 | self.子窗口信号.发送消息("发送给主窗口") 126 | ``` 127 | 128 | ## 代码示例 129 | 130 | ### 父窗口 main.py 131 | 132 | ``` 133 | import sys 134 | 135 | import qtawesome 136 | from PySide6 import QtWidgets, QtCore 137 | 138 | from PySide6.QtCore import QLocale, QTranslator, Signal 139 | from qtpy.uic import loadUi 140 | import qtefun 141 | import win_login 142 | from qtefun.消息通信 import 消息通信 143 | 144 | 145 | class MyWidget(qtefun.公共类): 146 | 主窗口信号 = 消息通信() 147 | 用户名 = "" 148 | 密码 = "" 149 | token="" 150 | 151 | def __init__(self): 152 | super().__init__() 153 | # 加载ui 154 | self.ui = loadUi("app.ui", self) 155 | # self.ui.show() 156 | # self.ui = Ui_Form() 157 | # self.ui.setupUi(self) 158 | self.ui.pushButton.setText("祖国 您好~") 159 | self.ui.textEdit.setText("祖国 您好~") 160 | self.setWindowTitle("祖国 您好~") 161 | 162 | fa5_icon = qtawesome.icon('fa5s.flag', 163 | color=('red', 100)) 164 | self.ui.pushButton.setIcon(fa5_icon) 165 | 166 | self.win_login = win_login.MyWidget() 167 | # self.win_login.子窗口信号.connect(self.子窗口消息) # 子窗口信号绑定主窗口的函数 168 | # self.主窗口信号.connect(self.win_login.父窗口消息) # 主窗口信号绑定子窗口的函数 169 | 170 | self.主窗口信号.接收消息(self.win_login.父窗口消息) 171 | self.win_login.子窗口信号.接收消息(self.子窗口消息) 172 | 173 | self.测试代码() 174 | 175 | def closeEvent(self, event): 176 | # 关闭窗口时,关闭子窗口 177 | sys.exit(0) 178 | 179 | def 子窗口消息(self, 消息内容): 180 | print("子窗口消息", 消息内容) 181 | if 消息内容['消息类型'] == "登录成功": 182 | self.用户名 = 消息内容['用户名'] 183 | self.密码 = 消息内容['密码'] 184 | self.token = 消息内容['密码'] 185 | self.ui.show() 186 | 187 | print("账户密码 {} {}".format(self.用户名,self.密码)) 188 | print("消息内容.消息类型 {}".format(消息内容['消息类型'])) 189 | self.setWindowTitle("欢迎回来,{}".format(self.用户名)) 190 | 191 | 192 | def 测试代码(self): 193 | pass 194 | self.win_login.show() 195 | self.主窗口信号.发送消息("发送给子窗口") 196 | 197 | # 返回结果 = self.打开文件选择器() 198 | # print(返回结果) 199 | # 返回结果 = self.打开文件夹选择器() 200 | # print(返回结果) 201 | # 返回结果 = self.打开颜色选择器() 202 | # print(返回结果) 203 | # 返回结果 = self.打开字体选择器() 204 | # print(返回结果) 205 | # 返回结果 = self.打开输入框("请输入内容","请输入内容") 206 | # print(返回结果) 207 | 208 | @QtCore.Slot() 209 | def on_pushButton_login_clicked(self): 210 | pass 211 | print("祖国 您好~") 212 | 213 | @QtCore.Slot() 214 | def on_pushButton_clicked(self): 215 | pass 216 | self.测试代码() 217 | 218 | # self.消息框("信息框","标题", 1, ["确定"]) 219 | # self.消息框("错误框","标题", 2, ["确定"]) 220 | # self.消息框("警告框","标题", 3, ["确定"]) 221 | # self.消息框("问题框","标题", 4, ["确定"]) 222 | # 返回结果 = self.消息框("信息框", "标题", 1, ["确定", "取消", "是", "否", "重试", "忽略"]) 223 | # print(返回结果) 224 | # self.消息框(F"你点击了{返回结果}") 225 | 226 | # info = self.ui.textEdit.toPlainText() 227 | # print(info) 228 | 229 | 230 | if __name__ == '__main__': 231 | QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) 232 | app = QtWidgets.QApplication([]) 233 | 234 | # 设置系统语言为中文 235 | qLocale = QLocale(QLocale.Chinese, QLocale.SimplifiedChineseScript) 236 | trans = QTranslator() 237 | trans.load("qt_zh_CN") 238 | app.installTranslator(trans) 239 | 240 | widget = MyWidget() 241 | # widget.show() 242 | sys.exit(app.exec()) 243 | 244 | ``` 245 | 246 | 247 | 248 | ### 子窗口 win_login.py 249 | 250 | ``` 251 | import sys 252 | from PySide6 import QtWidgets, QtCore 253 | 254 | from PySide6.QtCore import QLocale, QTranslator, Signal 255 | from PySide6.QtWidgets import QDialog 256 | from qtpy.uic import loadUi 257 | import qtefun 258 | 259 | from qtefun.消息通信 import 消息通信 260 | 261 | 262 | class MyWidget(QDialog,qtefun.公共类): 263 | 子窗口信号 = 消息通信() 264 | def __init__(self,parent=None): 265 | QDialog.__init__(self, parent) 266 | # 加载ui 267 | self.ui = loadUi("ui_login.ui", self) 268 | self.ui.show() 269 | 270 | def closeEvent(self, event): 271 | self.ui.lineEdit_user.setText("") 272 | self.ui.lineEdit_pass.setText("") 273 | 274 | def 父窗口消息(self, 消息内容): 275 | print("父窗口消息",消息内容) 276 | 277 | @QtCore.Slot() 278 | def on_pushButton_login_clicked(self): 279 | pass 280 | 用户名 = self.ui.lineEdit_user.text() 281 | 密码 = self.ui.lineEdit_pass.text() 282 | print(用户名, 密码) 283 | if 用户名 == "admin" and 密码 == "admin": 284 | self.子窗口信号.发送消息({ 285 | "消息类型": "登录成功", 286 | "用户名": 用户名, 287 | "密码": 密码, 288 | "token": "token" 289 | }) 290 | self.消息框("登录成功", "提示", 1, ["确定"]) 291 | self.close() 292 | 293 | else: 294 | self.子窗口信号.发送消息({ 295 | "消息类型": "登录失败", 296 | }) 297 | self.消息框("登录失败", "提示", 2, ["确定"]) 298 | 299 | 300 | if __name__ == '__main__': 301 | QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) 302 | app = QtWidgets.QApplication([]) 303 | widget = MyWidget() 304 | # widget.show() 305 | sys.exit(app.exec()) 306 | 307 | 308 | ``` 309 | 310 | -------------------------------------------------------------------------------- /go2tv_res/internal/gui/main_mobile.go: -------------------------------------------------------------------------------- 1 | //go:build android || ios 2 | // +build android ios 3 | 4 | package gui 5 | 6 | import ( 7 | "errors" 8 | "net/url" 9 | "sort" 10 | "time" 11 | 12 | "fyne.io/fyne/v2" 13 | "fyne.io/fyne/v2/canvas" 14 | "fyne.io/fyne/v2/container" 15 | "fyne.io/fyne/v2/layout" 16 | "fyne.io/fyne/v2/theme" 17 | "fyne.io/fyne/v2/widget" 18 | "github.com/alexballas/go2tv/devices" 19 | "github.com/alexballas/go2tv/soapcalls" 20 | "github.com/alexballas/go2tv/utils" 21 | ) 22 | 23 | func mainWindow(s *NewScreen) fyne.CanvasObject { 24 | w := s.Current 25 | 26 | list := new(widget.List) 27 | 28 | data := make([]devType, 0) 29 | 30 | w.Canvas().SetOnTypedKey(func(k *fyne.KeyEvent) { 31 | if k.Name == "Space" || k.Name == "P" { 32 | 33 | currentState := s.getScreenState() 34 | 35 | switch currentState { 36 | case "Playing": 37 | go pauseAction(s) 38 | case "Paused": 39 | go playAction(s) 40 | } 41 | } 42 | 43 | if k.Name == "S" { 44 | go stopAction(s) 45 | } 46 | }) 47 | 48 | go func() { 49 | datanew, err := getDevices(1) 50 | data = datanew 51 | if err != nil { 52 | data = nil 53 | } 54 | list.Refresh() 55 | }() 56 | 57 | mfiletext := widget.NewEntry() 58 | sfiletext := widget.NewEntry() 59 | 60 | mfile := widget.NewButton("Select Media File", func() { 61 | go mediaAction(s) 62 | }) 63 | 64 | mfiletext.Disable() 65 | 66 | sfile := widget.NewButton("Select Subtitles File", func() { 67 | go subsAction(s) 68 | }) 69 | 70 | sfiletext.Disable() 71 | 72 | playpause := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { 73 | go playAction(s) 74 | }) 75 | 76 | stop := widget.NewButtonWithIcon("Stop", theme.MediaStopIcon(), func() { 77 | go stopAction(s) 78 | }) 79 | 80 | volumeup := widget.NewButtonWithIcon("", theme.ContentAddIcon(), func() { 81 | go volumeAction(s, true) 82 | }) 83 | 84 | muteunmute := widget.NewButtonWithIcon("", theme.VolumeMuteIcon(), func() { 85 | go muteAction(s) 86 | }) 87 | 88 | volumedown := widget.NewButtonWithIcon("", theme.ContentRemoveIcon(), func() { 89 | go volumeAction(s, false) 90 | }) 91 | 92 | clearmedia := widget.NewButtonWithIcon("", theme.CancelIcon(), func() { 93 | go clearmediaAction(s) 94 | }) 95 | 96 | clearsubs := widget.NewButtonWithIcon("", theme.CancelIcon(), func() { 97 | go clearsubsAction(s) 98 | }) 99 | 100 | externalmedia := widget.NewCheck("Media from URL", func(b bool) {}) 101 | medialoop := widget.NewCheck("Loop Selected", func(b bool) {}) 102 | 103 | mediafilelabel := canvas.NewText("File:", nil) 104 | subsfilelabel := canvas.NewText("Subtitles:", nil) 105 | devicelabel := canvas.NewText("Select Device:", nil) 106 | 107 | list = widget.NewList( 108 | func() int { 109 | return len(data) 110 | }, 111 | func() fyne.CanvasObject { 112 | return container.NewHBox(widget.NewIcon(theme.NavigateNextIcon()), widget.NewLabel("Template Object")) 113 | }, 114 | func(i widget.ListItemID, o fyne.CanvasObject) { 115 | o.(*fyne.Container).Objects[1].(*widget.Label).SetText(data[i].name) 116 | }) 117 | 118 | s.PlayPause = playpause 119 | s.Stop = stop 120 | s.MuteUnmute = muteunmute 121 | s.ExternalMediaURL = externalmedia 122 | s.MediaText = mfiletext 123 | s.SubsText = sfiletext 124 | s.DeviceList = list 125 | 126 | actionbuttons := container.New(&mainButtonsLayout{buttonHeight: 1.5}, playpause, volumedown, muteunmute, volumeup, stop) 127 | 128 | checklists := container.NewHBox(externalmedia, medialoop) 129 | mediasubsbuttons := container.New(layout.NewGridLayout(2), mfile, sfile) 130 | sfiletextArea := container.New(layout.NewBorderLayout(nil, nil, nil, clearsubs), clearsubs, sfiletext) 131 | mfiletextArea := container.New(layout.NewBorderLayout(nil, nil, nil, clearmedia), clearmedia, mfiletext) 132 | viewfilescont := container.New(layout.NewFormLayout(), mediafilelabel, mfiletextArea, subsfilelabel, sfiletextArea) 133 | buttons := container.NewVBox(mediasubsbuttons, viewfilescont, checklists, actionbuttons, container.NewPadded(devicelabel)) 134 | content := container.New(layout.NewBorderLayout(buttons, nil, nil, nil), buttons, list) 135 | 136 | // Widgets actions 137 | list.OnSelected = func(id widget.ListItemID) { 138 | playpause.Enable() 139 | t, err := soapcalls.DMRextractor(data[id].addr) 140 | check(w, err) 141 | if err == nil { 142 | s.selectedDevice = data[id] 143 | s.controlURL, s.eventlURL, s.renderingControlURL = t.AvtransportControlURL, t.AvtransportEventSubURL, t.RenderingControlURL 144 | if s.tvdata != nil { 145 | s.tvdata.RenderingControlURL = s.renderingControlURL 146 | } 147 | } 148 | } 149 | 150 | externalmedia.OnChanged = func(b bool) { 151 | if b { 152 | mfile.Disable() 153 | 154 | // rename the label 155 | mediafilelabel.Text = "URL:" 156 | mediafilelabel.Refresh() 157 | 158 | // Clear the Media Text Area 159 | clearmediaAction(s) 160 | 161 | // Set some Media text defaults 162 | // to indicate that we're expecting a URL 163 | mfiletext.SetPlaceHolder("Enter URL here") 164 | mfiletext.Enable() 165 | return 166 | } 167 | 168 | medialoop.Enable() 169 | mfile.Enable() 170 | mediafilelabel.Text = "File:" 171 | mfiletext.SetPlaceHolder("") 172 | mfiletext.Text = "" 173 | mediafilelabel.Refresh() 174 | mfiletext.Disable() 175 | } 176 | 177 | medialoop.OnChanged = func(b bool) { 178 | s.Medialoop = b 179 | } 180 | 181 | // Device list auto-refresh 182 | go refreshDevList(s, &data) 183 | 184 | // Check mute status for selected device 185 | go checkMutefunc(s) 186 | 187 | return content 188 | } 189 | 190 | func refreshDevList(s *NewScreen, data *[]devType) { 191 | refreshDevices := time.NewTicker(5 * time.Second) 192 | 193 | w := s.Current 194 | 195 | _, err := getDevices(2) 196 | if err != nil && !errors.Is(err, devices.ErrNoDeviceAvailable) { 197 | check(w, err) 198 | } 199 | 200 | for range refreshDevices.C { 201 | datanew, _ := getDevices(2) 202 | oldListSize := len(*data) 203 | 204 | // check to see if the new refresh includes 205 | // one of the already selected devices 206 | var includes bool 207 | u, _ := url.Parse(s.controlURL) 208 | for _, d := range datanew { 209 | n, _ := url.Parse(d.addr) 210 | if n.Host == u.Host { 211 | includes = true 212 | } 213 | } 214 | 215 | *data = datanew 216 | 217 | if !includes { 218 | if utils.HostPortIsAlive(u.Host) { 219 | *data = append(*data, s.selectedDevice) 220 | sort.Slice(*data, func(i, j int) bool { 221 | return (*data)[i].name < (*data)[j].name 222 | }) 223 | 224 | } else { 225 | s.controlURL = "" 226 | s.DeviceList.UnselectAll() 227 | } 228 | } 229 | 230 | if oldListSize != len(*data) { 231 | // Something changed in the list, so we need to 232 | // also refresh the active selection. 233 | for n, a := range *data { 234 | if s.selectedDevice == a { 235 | s.DeviceList.Select(n) 236 | } 237 | } 238 | } 239 | 240 | s.DeviceList.Refresh() 241 | } 242 | } 243 | 244 | func checkMutefunc(s *NewScreen) { 245 | checkMute := time.NewTicker(1 * time.Second) 246 | 247 | var checkMuteCounter int 248 | for range checkMute.C { 249 | 250 | // Stop trying after 5 failures 251 | // to get the mute status 252 | if checkMuteCounter == 5 { 253 | s.renderingControlURL = "" 254 | checkMuteCounter = 0 255 | } 256 | 257 | if s.renderingControlURL == "" { 258 | continue 259 | } 260 | 261 | if s.tvdata == nil { 262 | s.tvdata = &soapcalls.TVPayload{RenderingControlURL: s.renderingControlURL} 263 | } 264 | 265 | isMuted, err := s.tvdata.GetMuteSoapCall() 266 | if err != nil { 267 | checkMuteCounter++ 268 | continue 269 | } 270 | 271 | checkMuteCounter = 0 272 | 273 | switch isMuted { 274 | case "1": 275 | setMuteUnmuteView("Unmute", s) 276 | case "0": 277 | setMuteUnmuteView("Mute", s) 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /qtefun/部件公共类.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets 2 | from PySide6.QtCore import Qt 3 | from PySide6.QtGui import QImage, QColor 4 | 5 | 6 | class 部件公共类(QtWidgets.QWidget): 7 | 8 | def 消息框(self, 内容="", 标题="", 类型=1, 按钮: list = None): 9 | """ 10 | 消息框 11 | 12 | :param 内容: 提示的内容 13 | :param 标题: 窗口的标题 14 | :param 类型: 1.消息框 2.错误框 3.警告框 4.问题框 15 | :param 按钮: 按钮的文本 参数 ["确定","取消","是","否","重试","忽略"] 如果为空则为确定按钮 16 | 17 | :return: 返回点击的按钮文本 18 | """ 19 | if 按钮 is None: 20 | 按钮 = ["确定"] 21 | 22 | 按钮列表 = { 23 | "确定": QtWidgets.QMessageBox.Ok, 24 | "取消": QtWidgets.QMessageBox.Cancel, 25 | "是": QtWidgets.QMessageBox.Yes, 26 | "否": QtWidgets.QMessageBox.No, 27 | "重试": QtWidgets.QMessageBox.Retry, 28 | "忽略": QtWidgets.QMessageBox.Ignore, 29 | } 30 | # key value 互换 31 | 按钮列表2 = {value: key for key, value in 按钮列表.items()} 32 | 33 | # 检查按钮的参数 循环 按钮列表 组合参数 34 | 按钮参数 = None 35 | for 按钮文本 in 按钮: 36 | if 按钮参数 is None: 37 | 按钮参数 = 按钮列表[按钮文本] 38 | else: 39 | 按钮参数 = 按钮参数 | 按钮列表[按钮文本] 40 | 41 | if 类型 == 1: 42 | 返回结果 = QtWidgets.QMessageBox.information(self, 标题, 内容, buttons=按钮参数) 43 | elif 类型 == 2: 44 | 返回结果 = QtWidgets.QMessageBox.warning(self, 标题, 内容, buttons=按钮参数) 45 | elif 类型 == 3: 46 | 返回结果 = QtWidgets.QMessageBox.critical(self, 标题, 内容, buttons=按钮参数) 47 | elif 类型 == 4: 48 | 返回结果 = QtWidgets.QMessageBox.question(self, 标题, 内容, buttons=按钮参数) 49 | # print(返回结果) 50 | # 匹配 按钮列表 和 返回结果 51 | 返回结果 = 按钮列表2[返回结果] 52 | # print(返回结果) 53 | 54 | return 返回结果 55 | 56 | def 打开文件选择器(self, 文件类型: str = None, 标题="打开文件", 初始目录="."): 57 | """ 58 | 打开文件选择器 59 | 60 | :param 文件类型: 例如: "所有文件 (*);;文本文件 (*.txt)" 61 | :param 标题: 62 | :param 初始目录: 默认当前目录 63 | :return: 返回选择的文件路径 64 | """ 65 | if 文件类型 is None: 66 | 文件类型 = "所有文件 (*);;文本文件 (*.txt)" 67 | 文件路径, _ = QtWidgets.QFileDialog.getOpenFileName(self, 标题, 初始目录, 文件类型) 68 | return 文件路径 69 | 70 | def 打开文件夹选择器(self, 标题="打开文件夹", 初始目录="."): 71 | """ 72 | 打开文件夹选择器 73 | 74 | :param 标题: 75 | :param 初始目录: 默认当前目录 76 | :return: 返回选择的文件夹路径 77 | """ 78 | 文件夹路径 = QtWidgets.QFileDialog.getExistingDirectory(self, 标题, 初始目录) 79 | return 文件夹路径 80 | 81 | def 打开文件保存选择器(self, 文件类型: str = None, 标题="保存文件", 初始目录="."): 82 | """ 83 | 打开文件保存选择器 84 | 85 | :param 文件类型: 例如: "所有文件 (*);;文本文件 (*.txt)" 86 | :param 标题: 87 | :param 初始目录: 默认当前目录 88 | :return: 返回选择的文件路径 89 | """ 90 | 文件类型 = QtWidgets.QFileDialog.getSaveFileName(self, 标题, 初始目录, 文件类型) 91 | return 文件类型 92 | 93 | def 打开颜色选择器(self): 94 | """ 95 | 打开颜色选择器 96 | 97 | :return: 返回选择的颜色 例如 PySide6.QtGui.QColor.fromRgbF(0.362097, 0.190341, 0.397406, 1.000000) 98 | """ 99 | color = QtWidgets.QColorDialog.getColor() 100 | return color 101 | 102 | def 打开字体选择器(self): 103 | """ 104 | 打开字体选择器 105 | 106 | :return: 返回选择的字体 例如 107 | """ 108 | _, font = QtWidgets.QFontDialog.getFont() 109 | return font 110 | 111 | def 打开输入框(self, 标题="输入", 内容="请输入", 初始值="", 密码=False): 112 | """ 113 | 打开输入框 114 | 115 | :param 标题: 标题 116 | :param 内容: 内容 117 | :param 初始值: 默认为空 118 | :param 密码: 是否是密码框 119 | :return: 返回输入的值 例如 "123" , True 120 | """ 121 | if 密码: 122 | 输入结果, 确定 = QtWidgets.QInputDialog.getText(self, 标题, 内容, QtWidgets.QLineEdit.Password, 初始值) 123 | else: 124 | 输入结果, 确定 = QtWidgets.QInputDialog.getText(self, 标题, 内容, QtWidgets.QLineEdit.Normal, 初始值) 125 | 126 | return 输入结果, 确定 127 | 128 | # 获取窗口标题 129 | @property 130 | def 标题(self): 131 | return self.windowTitle() 132 | 133 | # 设置窗口标题 134 | @标题.setter 135 | def 标题(self, value: str): 136 | return self.setWindowTitle(value) 137 | 138 | # 获取窗口宽度 139 | @property 140 | def 宽度(self): 141 | return self.width() 142 | 143 | # 设置窗口宽度 144 | @宽度.setter 145 | def 宽度(self, value: int): 146 | return self.setFixedWidth(value) 147 | 148 | # 获取窗口高度 149 | @property 150 | def 高度(self): 151 | return self.height() 152 | 153 | # 设置窗口高度 154 | @高度.setter 155 | def 高度(self, value: int): 156 | return self.setFixedHeight(value) 157 | 158 | # 获取窗口左边 159 | @property 160 | def 左边(self): 161 | return self.x() 162 | 163 | # 设置窗口左边 164 | @左边.setter 165 | def 左边(self, value: int): 166 | return self.move(value, self.y()) 167 | 168 | # 获取窗口上边 169 | @property 170 | def 顶边(self): 171 | return self.y() 172 | 173 | # 设置窗口顶边 174 | @顶边.setter 175 | def 顶边(self, value: int): 176 | return self.move(self.x(), value) 177 | 178 | # 获取窗口右边 179 | @property 180 | def 右边(self): 181 | return self.x() + self.width() 182 | 183 | # 设置窗口右边 184 | @右边.setter 185 | def 右边(self, value: int): 186 | return self.move(value - self.width(), self.y()) 187 | 188 | # 获取窗口下边 189 | @property 190 | def 下边(self): 191 | return self.y() + self.height() 192 | 193 | # 设置窗口下边 194 | @下边.setter 195 | def 下边(self, value: int): 196 | return self.move(self.x(), value - self.height()) 197 | 198 | # 获取窗口大小 199 | @property 200 | def 大小(self): 201 | return self.size() 202 | 203 | # 设置窗口大小 204 | @大小.setter 205 | def 大小(self, value: tuple): 206 | return self.setFixedSize(value) 207 | 208 | # 获取窗口位置 209 | @property 210 | def 位置(self): 211 | return self.pos() 212 | 213 | # 设置窗口位置 214 | @位置.setter 215 | def 位置(self, value: tuple): 216 | return self.move(value) 217 | 218 | # 获取窗口是否可见 219 | @property 220 | def 可视(self): 221 | return self.isVisible() 222 | 223 | # 设置窗口是否可见 224 | @可视.setter 225 | def 可视(self, value: bool): 226 | return self.setVisible(value) 227 | 228 | # 获取屏幕宽度 229 | def 取屏幕宽度(self): 230 | return self.screen().physicalSize().width() 231 | 232 | # 获取屏幕高度 233 | def 取屏幕高度(self): 234 | return self.screen().physicalSize().height() 235 | 236 | # 获取屏幕分辨率 237 | def 取屏幕分辨率(self): 238 | return self.screen().physicalSize() 239 | 240 | # 移动 241 | def 移动(self, x: int, y: int): 242 | return self.move(x, y) 243 | 244 | # 移动到屏幕中间 245 | def 移动到屏幕中间(self): 246 | return self.move(self.取屏幕宽度() / 2 - self.width() / 2, self.取屏幕高度() / 2 - self.height() / 2) 247 | 248 | # 移动到屏幕右下角 249 | def 移动到屏幕右下角(self): 250 | return self.move(self.取屏幕宽度() - self.width(), self.取屏幕高度() - self.height()) 251 | 252 | # 移动到屏幕左上角 253 | def 移动到屏幕左上角(self): 254 | return self.move(0, 0) 255 | 256 | # 移动到屏幕左下角 257 | def 移动到屏幕左下角(self): 258 | return self.move(0, self.取屏幕高度() - self.height()) 259 | 260 | # 移动到屏幕右上角 261 | def 移动到屏幕右上角(self): 262 | return self.move(self.取屏幕宽度() - self.width(), 0) 263 | 264 | # 移动到屏幕中间 265 | def 移动到屏幕中间(self): 266 | return self.move(self.取屏幕宽度() / 2 - self.width() / 2, self.取屏幕高度() / 2 - self.height() / 2) 267 | 268 | # 设置背景颜色 269 | def 设置背景颜色(self, color: QColor): 270 | return self.setStyleSheet("background-color: %s;" % color.name()) 271 | 272 | # 设置背景图片 273 | def 设置背景图片(self, image: QImage): 274 | return self.setStyleSheet("background-image: url(%s);" % image.toImage().toBase64().decode()) 275 | 276 | def 置鼠标样式(self, 样式: Qt.CursorShape): 277 | ''' 278 | https://doc.qt.io/qtforpython/PySide6/QtCore/Qt.html?highlight=cursorshape#PySide6.QtCore.PySide6.QtCore.Qt.CursorShape 279 | ''' 280 | return self.setCursor(样式) 281 | 282 | # 设置提示文本 283 | def 设置提示文本(self, text: str): 284 | return self.setToolTip(text) 285 | -------------------------------------------------------------------------------- /go2tv_res/cmd/go2tv-lite/go2tv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "net/url" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "runtime" 14 | "sort" 15 | "strings" 16 | "sync" 17 | "time" 18 | 19 | "github.com/alexballas/go2tv/devices" 20 | "github.com/alexballas/go2tv/httphandlers" 21 | "github.com/alexballas/go2tv/soapcalls" 22 | "github.com/alexballas/go2tv/utils" 23 | "github.com/pkg/errors" 24 | ) 25 | 26 | var ( 27 | //go:embed version.txt 28 | version string 29 | errNoflag = errors.New("没有使用标志") 30 | 视频音频文件的本地路径 = flag.String("v", "", "视频音频文件的本地路径。 (触发 CLI 模式)") 31 | 媒体文件的 = flag.String("u", "", "媒体文件的 HTTP URL。 URL 流不支持查找操作。 (触发 CLI 模式)") 32 | 字幕文件的本地路径 = flag.String("s", "", "字幕文件的本地路径。") 33 | 目标URL = flag.String("t", "", "投射到特定的 UPnP DLNA 媒体渲染器 URL。") 34 | 文件转码路径 = flag.Bool("tc", false, "使用 ffmpeg 对输入视频文件进行转码。") 35 | 所有设备列表 = flag.Bool("l", false, "列出所有可用的 UPnPDLNA 媒体渲染器模型和 URL。") 36 | 37 | 版本号 = flag.Bool("version", false, "版本号。") 38 | ) 39 | 40 | type flagResults struct { 41 | dmrURL string 42 | exit bool 43 | } 44 | 45 | func main() { 46 | var absMediaFile string 47 | var mediaType string 48 | var 媒体文件 interface{} 49 | var isSeek bool 50 | 51 | flag.Parse() 52 | 53 | flagRes, err := processflags() 54 | check(err) 55 | 56 | if flagRes.exit { 57 | os.Exit(0) 58 | } 59 | 60 | if *视频音频文件的本地路径 != "" { 61 | 媒体文件 = *视频音频文件的本地路径 62 | } 63 | 64 | if *视频音频文件的本地路径 == "" && *媒体文件的 != "" { 65 | mediaURL, err := utils.StreamURL(context.Background(), *媒体文件的) 66 | check(err) 67 | 68 | mediaURLinfo, err := utils.StreamURL(context.Background(), *媒体文件的) 69 | check(err) 70 | 71 | mediaType, err = utils.GetMimeDetailsFromStream(mediaURLinfo) 72 | check(err) 73 | 74 | 媒体文件 = mediaURL 75 | 76 | if strings.Contains(mediaType, "image") { 77 | readerToBytes, err := io.ReadAll(mediaURL) 78 | mediaURL.Close() 79 | check(err) 80 | 媒体文件 = readerToBytes 81 | } 82 | } 83 | 84 | switch t := 媒体文件.(type) { 85 | case string: 86 | absMediaFile, err = filepath.Abs(t) 87 | check(err) 88 | 89 | mfile, err := os.Open(absMediaFile) 90 | check(err) 91 | 92 | 媒体文件 = absMediaFile 93 | mediaType, err = utils.GetMimeDetailsFromFile(mfile) 94 | 95 | if !*文件转码路径 { 96 | isSeek = true 97 | } 98 | 99 | check(err) 100 | case io.ReadCloser, []byte: 101 | absMediaFile = *媒体文件的 102 | } 103 | 104 | 字幕文件, err := filepath.Abs(*字幕文件的本地路径) 105 | check(err) 106 | 107 | upnpServicesURLs, err := soapcalls.DMRextractor(flagRes.dmrURL) 108 | check(err) 109 | 110 | whereToListen, err := utils.URLtoListenIPandPort(flagRes.dmrURL) 111 | check(err) 112 | 113 | //scr, err := interactive.InitTcellNewScreen() 114 | //check(err) 115 | 116 | callbackPath, err := utils.RandomString() 117 | check(err) 118 | 119 | tvdata := &soapcalls.TVPayload{ 120 | ControlURL: upnpServicesURLs.AvtransportControlURL, 121 | EventURL: upnpServicesURLs.AvtransportEventSubURL, 122 | RenderingControlURL: upnpServicesURLs.RenderingControlURL, 123 | CallbackURL: "http://" + whereToListen + "/" + callbackPath, 124 | MediaURL: "http://" + whereToListen + "/" + utils.ConvertFilename(absMediaFile), 125 | SubtitlesURL: "http://" + whereToListen + "/" + utils.ConvertFilename(字幕文件), 126 | MediaType: mediaType, 127 | CurrentTimers: make(map[string]*time.Timer), 128 | MediaRenderersStates: make(map[string]*soapcalls.States), 129 | InitialMediaRenderersStates: make(map[string]bool), 130 | RWMutex: &sync.RWMutex{}, 131 | Transcode: *文件转码路径, 132 | Seekable: isSeek, 133 | } 134 | 135 | s := httphandlers.NewServer(whereToListen) 136 | 服务器启动 := make(chan struct{}) 137 | 138 | //我们在这里传递 tvdata,因为我们需要回调处理程序能够对不同的媒体渲染器状态做出反应。 139 | go func() { 140 | err := s.StartServer(服务器启动, 媒体文件, 字幕文件, tvdata) 141 | check(err) 142 | }() 143 | // 等待HTTP服务器正确初始化 144 | <-服务器启动 145 | if err := tvdata.SendtoTV("Play1"); err != nil { 146 | fmt.Fprintf(os.Stderr, "%v\n", err) 147 | os.Exit(1) 148 | } 149 | <-服务器启动 150 | } 151 | 152 | func check(err error) { 153 | if errors.Is(err, errNoflag) { 154 | flag.Usage() 155 | os.Exit(0) 156 | } 157 | 158 | if err != nil { 159 | _, _ = fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err) 160 | os.Exit(1) 161 | } 162 | } 163 | 164 | func listFlagFunction() error { 165 | flagsEnabled := 0 166 | flag.Visit(func(f *flag.Flag) { 167 | flagsEnabled++ 168 | }) 169 | 170 | if flagsEnabled > 1 { 171 | return errors.New("cant combine -l with other flags") 172 | } 173 | 174 | deviceList, err := devices.LoadSSDPservices(1) 175 | if err != nil { 176 | return errors.New("failed to list devices") 177 | } 178 | 179 | fmt.Println() 180 | 181 | // We loop through this map twice as we need to maintain 182 | // the correct order. 183 | keys := make([]string, 0) 184 | for k := range deviceList { 185 | keys = append(keys, k) 186 | } 187 | 188 | sort.Strings(keys) 189 | 190 | for q, k := range keys { 191 | boldStart := "" 192 | boldEnd := "" 193 | 194 | if runtime.GOOS == "linux" { 195 | boldStart = "\033[1m" 196 | boldEnd = "\033[0m" 197 | } 198 | fmt.Printf("%sDevice %v%s\n", boldStart, q+1, boldEnd) 199 | fmt.Printf("%s--------%s\n", boldStart, boldEnd) 200 | fmt.Printf("%sModel:%s %s\n", boldStart, boldEnd, k) 201 | fmt.Printf("%sURL:%s %s\n", boldStart, boldEnd, deviceList[k]) 202 | fmt.Println() 203 | } 204 | 205 | return nil 206 | } 207 | 208 | func processflags() (*flagResults, error) { 209 | checkVerflag() 210 | 211 | res := &flagResults{} 212 | 213 | if *视频音频文件的本地路径 == "" && !*所有设备列表 && *媒体文件的 == "" { 214 | return nil, fmt.Errorf("checkflags error: %w", errNoflag) 215 | } 216 | 217 | if err := checkTCflag(res); err != nil { 218 | return nil, fmt.Errorf("checkflags error: %w", err) 219 | } 220 | 221 | if err := checkTflag(res); err != nil { 222 | return nil, fmt.Errorf("checkflags error: %w", err) 223 | } 224 | 225 | list, err := checkLflag() 226 | if err != nil { 227 | return nil, fmt.Errorf("checkflags error: %w", err) 228 | } 229 | 230 | if list { 231 | res.exit = true 232 | return res, nil 233 | } 234 | 235 | if err := checkVflag(); err != nil { 236 | return nil, fmt.Errorf("checkflags error: %w", err) 237 | } 238 | 239 | if err := checkSflag(); err != nil { 240 | return nil, fmt.Errorf("checkflags error: %w", err) 241 | } 242 | 243 | return res, nil 244 | } 245 | 246 | func checkVflag() error { 247 | if !*所有设备列表 && *媒体文件的 == "" { 248 | if _, err := os.Stat(*视频音频文件的本地路径); os.IsNotExist(err) { 249 | return fmt.Errorf("checkVflags error: %w", err) 250 | } 251 | } 252 | 253 | return nil 254 | } 255 | 256 | func checkSflag() error { 257 | if *字幕文件的本地路径 != "" { 258 | if _, err := os.Stat(*字幕文件的本地路径); os.IsNotExist(err) { 259 | return fmt.Errorf("checkSflags error: %w", err) 260 | } 261 | return nil 262 | } 263 | 264 | // The checkVflag should happen before checkSflag so we're safe to call 265 | // *视频音频文件的本地路径 here. If *字幕文件的本地路径 is empty, try to automatically find the 266 | // srt from the media file filename. 267 | *字幕文件的本地路径 = (*视频音频文件的本地路径)[0:len(*视频音频文件的本地路径)- 268 | len(filepath.Ext(*视频音频文件的本地路径))] + ".srt" 269 | 270 | return nil 271 | } 272 | 273 | func checkTCflag(res *flagResults) error { 274 | if *文件转码路径 { 275 | _, err := exec.LookPath("ffmpeg") 276 | if err != nil { 277 | return fmt.Errorf("checkTCflag parse error: %w", err) 278 | } 279 | } 280 | 281 | return nil 282 | } 283 | 284 | func checkTflag(res *flagResults) error { 285 | if *目标URL != "" { 286 | // Validate URL before proceeding. 287 | _, err := url.ParseRequestURI(*目标URL) 288 | if err != nil { 289 | return fmt.Errorf("checkTflag parse error: %w", err) 290 | } 291 | 292 | res.dmrURL = *目标URL 293 | return nil 294 | } 295 | 296 | deviceList, err := devices.LoadSSDPservices(1) 297 | if err != nil { 298 | return fmt.Errorf("checkTflag service loading error: %w", err) 299 | } 300 | 301 | res.dmrURL, err = devices.DevicePicker(deviceList, 1) 302 | if err != nil { 303 | return fmt.Errorf("checkTflag device picker error: %w", err) 304 | } 305 | 306 | return nil 307 | } 308 | 309 | func checkLflag() (bool, error) { 310 | if *所有设备列表 { 311 | if err := listFlagFunction(); err != nil { 312 | return false, fmt.Errorf("checkLflag error: %w", err) 313 | } 314 | return true, nil 315 | } 316 | 317 | return false, nil 318 | } 319 | 320 | func checkVerflag() { 321 | if *版本号 && os.Args[1] == "-version" { 322 | fmt.Printf("Go2TV Version: %s\n", version) 323 | os.Exit(0) 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /ui_多多投屏.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'ui_多多投屏.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.3.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QGridLayout, 19 | QHBoxLayout, QLabel, QLineEdit, QMainWindow, 20 | QMenuBar, QPushButton, QSizePolicy, QStatusBar, 21 | QVBoxLayout, QWidget) 22 | import app_rc 23 | 24 | class Ui_MainWindow(object): 25 | def setupUi(self, MainWindow): 26 | if not MainWindow.objectName(): 27 | MainWindow.setObjectName(u"MainWindow") 28 | MainWindow.resize(397, 265) 29 | icon = QIcon() 30 | icon.addFile(u":/app/app.png", QSize(), QIcon.Normal, QIcon.Off) 31 | MainWindow.setWindowIcon(icon) 32 | self.centralwidget = QWidget(MainWindow) 33 | self.centralwidget.setObjectName(u"centralwidget") 34 | self.verticalLayout = QVBoxLayout(self.centralwidget) 35 | self.verticalLayout.setObjectName(u"verticalLayout") 36 | self.gridLayout = QGridLayout() 37 | self.gridLayout.setObjectName(u"gridLayout") 38 | self.pushButton_xuanzewenjjian = QPushButton(self.centralwidget) 39 | self.pushButton_xuanzewenjjian.setObjectName(u"pushButton_xuanzewenjjian") 40 | sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 41 | sizePolicy.setHorizontalStretch(0) 42 | sizePolicy.setVerticalStretch(0) 43 | sizePolicy.setHeightForWidth(self.pushButton_xuanzewenjjian.sizePolicy().hasHeightForWidth()) 44 | self.pushButton_xuanzewenjjian.setSizePolicy(sizePolicy) 45 | self.pushButton_xuanzewenjjian.setMinimumSize(QSize(0, 32)) 46 | 47 | self.gridLayout.addWidget(self.pushButton_xuanzewenjjian, 3, 1, 1, 1) 48 | 49 | self.label = QLabel(self.centralwidget) 50 | self.label.setObjectName(u"label") 51 | sizePolicy1 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) 52 | sizePolicy1.setHorizontalStretch(0) 53 | sizePolicy1.setVerticalStretch(0) 54 | sizePolicy1.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) 55 | self.label.setSizePolicy(sizePolicy1) 56 | self.label.setMaximumSize(QSize(16777215, 30)) 57 | 58 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 59 | 60 | self.lineEdit_lujing = QLineEdit(self.centralwidget) 61 | self.lineEdit_lujing.setObjectName(u"lineEdit_lujing") 62 | sizePolicy.setHeightForWidth(self.lineEdit_lujing.sizePolicy().hasHeightForWidth()) 63 | self.lineEdit_lujing.setSizePolicy(sizePolicy) 64 | self.lineEdit_lujing.setMinimumSize(QSize(0, 32)) 65 | 66 | self.gridLayout.addWidget(self.lineEdit_lujing, 3, 0, 1, 1) 67 | 68 | self.pushButton_shuaxin = QPushButton(self.centralwidget) 69 | self.pushButton_shuaxin.setObjectName(u"pushButton_shuaxin") 70 | sizePolicy.setHeightForWidth(self.pushButton_shuaxin.sizePolicy().hasHeightForWidth()) 71 | self.pushButton_shuaxin.setSizePolicy(sizePolicy) 72 | self.pushButton_shuaxin.setMinimumSize(QSize(0, 32)) 73 | 74 | self.gridLayout.addWidget(self.pushButton_shuaxin, 1, 1, 1, 1) 75 | 76 | self.label_2 = QLabel(self.centralwidget) 77 | self.label_2.setObjectName(u"label_2") 78 | sizePolicy2 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 79 | sizePolicy2.setHorizontalStretch(0) 80 | sizePolicy2.setVerticalStretch(0) 81 | sizePolicy2.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth()) 82 | self.label_2.setSizePolicy(sizePolicy2) 83 | self.label_2.setMaximumSize(QSize(16777215, 24)) 84 | 85 | self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) 86 | 87 | self.comboBox_shebeiliebiao = QComboBox(self.centralwidget) 88 | self.comboBox_shebeiliebiao.setObjectName(u"comboBox_shebeiliebiao") 89 | sizePolicy3 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 90 | sizePolicy3.setHorizontalStretch(1) 91 | sizePolicy3.setVerticalStretch(0) 92 | sizePolicy3.setHeightForWidth(self.comboBox_shebeiliebiao.sizePolicy().hasHeightForWidth()) 93 | self.comboBox_shebeiliebiao.setSizePolicy(sizePolicy3) 94 | self.comboBox_shebeiliebiao.setMinimumSize(QSize(0, 32)) 95 | 96 | self.gridLayout.addWidget(self.comboBox_shebeiliebiao, 1, 0, 1, 1) 97 | 98 | self.checkBox = QCheckBox(self.centralwidget) 99 | self.checkBox.setObjectName(u"checkBox") 100 | 101 | self.gridLayout.addWidget(self.checkBox, 4, 0, 1, 1) 102 | 103 | 104 | self.verticalLayout.addLayout(self.gridLayout) 105 | 106 | self.horizontalLayout = QHBoxLayout() 107 | self.horizontalLayout.setObjectName(u"horizontalLayout") 108 | self.pushButton_jianchagengxin = QPushButton(self.centralwidget) 109 | self.pushButton_jianchagengxin.setObjectName(u"pushButton_jianchagengxin") 110 | sizePolicy.setHeightForWidth(self.pushButton_jianchagengxin.sizePolicy().hasHeightForWidth()) 111 | self.pushButton_jianchagengxin.setSizePolicy(sizePolicy) 112 | self.pushButton_jianchagengxin.setMinimumSize(QSize(0, 32)) 113 | 114 | self.horizontalLayout.addWidget(self.pushButton_jianchagengxin, 0, Qt.AlignLeft) 115 | 116 | self.pushButton_kaishibofang = QPushButton(self.centralwidget) 117 | self.pushButton_kaishibofang.setObjectName(u"pushButton_kaishibofang") 118 | sizePolicy.setHeightForWidth(self.pushButton_kaishibofang.sizePolicy().hasHeightForWidth()) 119 | self.pushButton_kaishibofang.setSizePolicy(sizePolicy) 120 | self.pushButton_kaishibofang.setMinimumSize(QSize(0, 32)) 121 | 122 | self.horizontalLayout.addWidget(self.pushButton_kaishibofang) 123 | 124 | self.pushButton_tingzhibofang = QPushButton(self.centralwidget) 125 | self.pushButton_tingzhibofang.setObjectName(u"pushButton_tingzhibofang") 126 | sizePolicy.setHeightForWidth(self.pushButton_tingzhibofang.sizePolicy().hasHeightForWidth()) 127 | self.pushButton_tingzhibofang.setSizePolicy(sizePolicy) 128 | self.pushButton_tingzhibofang.setMinimumSize(QSize(0, 32)) 129 | 130 | self.horizontalLayout.addWidget(self.pushButton_tingzhibofang) 131 | 132 | 133 | self.verticalLayout.addLayout(self.horizontalLayout) 134 | 135 | MainWindow.setCentralWidget(self.centralwidget) 136 | self.menubar = QMenuBar(MainWindow) 137 | self.menubar.setObjectName(u"menubar") 138 | self.menubar.setGeometry(QRect(0, 0, 397, 24)) 139 | MainWindow.setMenuBar(self.menubar) 140 | self.statusbar = QStatusBar(MainWindow) 141 | self.statusbar.setObjectName(u"statusbar") 142 | MainWindow.setStatusBar(self.statusbar) 143 | 144 | self.retranslateUi(MainWindow) 145 | 146 | QMetaObject.connectSlotsByName(MainWindow) 147 | # setupUi 148 | 149 | def retranslateUi(self, MainWindow): 150 | MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"\u591a\u591a\u6295\u5c4f", None)) 151 | self.pushButton_xuanzewenjjian.setText(QCoreApplication.translate("MainWindow", u"\u9009\u62e9\u6587\u4ef6", None)) 152 | self.label.setText(QCoreApplication.translate("MainWindow", u"\u8bbe\u5907\u5217\u8868", None)) 153 | self.pushButton_shuaxin.setText(QCoreApplication.translate("MainWindow", u"\u5237\u65b0", None)) 154 | self.label_2.setText(QCoreApplication.translate("MainWindow", u"\u6295\u5c4f\u7684\u6587\u4ef6\u8def\u5f84 / Url \u5730\u5740", None)) 155 | self.checkBox.setText(QCoreApplication.translate("MainWindow", u"\u62d6\u653e\u6587\u4ef6\u540e\u76f4\u63a5\u5f00\u59cb\u64ad\u653e", None)) 156 | self.pushButton_jianchagengxin.setText(QCoreApplication.translate("MainWindow", u"\u68c0\u67e5\u66f4\u65b0", None)) 157 | self.pushButton_kaishibofang.setText(QCoreApplication.translate("MainWindow", u"\u5f00\u59cb\u64ad\u653e", None)) 158 | self.pushButton_tingzhibofang.setText(QCoreApplication.translate("MainWindow", u"\u505c\u6b62\u64ad\u653e", None)) 159 | # retranslateUi 160 | 161 | -------------------------------------------------------------------------------- /easy_to_tv.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import * 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | 5 | from qtefun.国际化 import 设置语言为中文 6 | from qtefun.组件.主窗口 import 主窗口 7 | from qtefun.组件.按钮 import 按钮 8 | from qtefun.组件.标签 import 标签 9 | from qtefun.组件.组合框 import 组合框 10 | from qtefun.组件.单行编辑框 import 单行编辑框 11 | from qtefun.组件.复选框 import 复选框 12 | from qtefun.图标 import * 13 | from pyefun.调试.调试输出 import * 14 | import 投屏模块 15 | import go2tv模块 16 | 17 | import ui_多多投屏 18 | import 文件服务器 19 | 20 | import qtAutoUpdateApp.自动更新模块 as 自动更新模块 21 | import version 22 | from pyefun import * 23 | 24 | 全局变量_版本号 = version.version 25 | 全局_项目名称 = "duolabmeng6/easy_to_tv" 26 | 全局_应用名称 = "easy_to_tv.app" 27 | 全局_当前版本 = version.version 28 | 全局_官方网址 = "https://github.com/duolabmeng6/easy_to_tv" 29 | 30 | 31 | class 文件服务器线程(QThread): 32 | def __init__(self): 33 | super(文件服务器线程, self).__init__() 34 | self.started.connect(self.ui_开始) 35 | self.finished.connect(self.ui_结束) 36 | 37 | def run(self): 38 | pass 39 | # 文w件服务器.app.run(host='0.0.0.0', port=6161, threaded=True, use_reloader=False, debug=False) 40 | kwargs = {'host': '0.0.0.0', 'port': 6161, 'threaded': True, 'use_reloader': False, 'debug': False} 41 | threading.Thread(target=文件服务器.app.run, daemon=True, kwargs=kwargs).start() 42 | 43 | def ui_开始(self): 44 | pass 45 | 46 | def ui_结束(self): 47 | pass 48 | 49 | 50 | class 刷新设备线程(QThread): 51 | def __init__(self, 回调函数): 52 | super(刷新设备线程, self).__init__() 53 | self.started.connect(self.ui_开始) 54 | self.finished.connect(self.ui_结束) 55 | self.回调函数 = 回调函数 56 | self.数据 = None 57 | 58 | def run(self): 59 | pass 60 | print("刷新设备线程开始") 61 | # self.数据 = 投屏模块.获取设备列表() 62 | self.数据 = go2tv模块.获取设备列表() 63 | 64 | def ui_开始(self): 65 | pass 66 | 67 | def ui_结束(self): 68 | pass 69 | self.回调函数(self.数据) 70 | 71 | 72 | class 投屏线程(QThread): 73 | def __init__(self, 当前选中设备URL, 文件路径, 回调函数): 74 | super(投屏线程, self).__init__() 75 | self.started.connect(self.ui_开始) 76 | self.finished.connect(self.ui_结束) 77 | self.当前选中设备URL = 当前选中设备URL 78 | self.文件路径 = 文件路径 79 | self.回调函数 = 回调函数 80 | self.播放设备 = None 81 | self.播放地址 = None 82 | 83 | def run(self): 84 | pass 85 | self.播放设备, self.播放地址 = 投屏模块.投递视频文件(self.当前选中设备URL, self.文件路径) 86 | 87 | def ui_开始(self): 88 | pass 89 | 90 | def ui_结束(self): 91 | pass 92 | self.回调函数(self.播放设备, self.播放地址) 93 | 94 | 95 | class MainWin(主窗口): 96 | 播放设备 = None 97 | 检查更新窗口 = None 98 | 99 | def __init__(self): 100 | super().__init__() 101 | self.setWindowFlags(Qt.WindowCloseButtonHint) 102 | 103 | self.当前选中设备URL = None 104 | self.ui = ui_多多投屏.Ui_MainWindow() 105 | self.ui.setupUi(self) 106 | self.show() 107 | 108 | # 禁止最大化 伸缩大小 109 | self.setFixedSize(self.width(), self.height()) 110 | self.setWindowTitle("多多投屏 " + version.version) 111 | # 状态条插入标签 112 | self.label_zhuangtaitiao = QLabel(self) 113 | # 点击事件 114 | self.标签状态条 = 标签(self.label_zhuangtaitiao) 115 | self.标签状态条.绑定事件被按下(self.标签状态条被点击) 116 | self.statusBar().addWidget(self.label_zhuangtaitiao) 117 | 118 | self.注册托盘图标() 119 | 120 | self.按钮刷新 = 按钮(self.ui.pushButton_shuaxin) 121 | self.按钮刷新.绑定事件被点击(self.按钮刷新被点击) 122 | self.按钮选择文件 = 按钮(self.ui.pushButton_xuanzewenjjian) 123 | self.按钮选择文件.绑定事件被点击(self.按钮选择文件被点击) 124 | self.按钮开始播放 = 按钮(self.ui.pushButton_kaishibofang) 125 | self.按钮开始播放.绑定事件被点击(self.按钮开始播放被点击) 126 | self.按钮停止播放 = 按钮(self.ui.pushButton_tingzhibofang) 127 | self.按钮停止播放.绑定事件被点击(self.按钮停止播放被点击) 128 | self.组合框设备列表 = 组合框(self.ui.comboBox_shebeiliebiao) 129 | self.组合框设备列表.绑定事件项目被选择(self.组合框项目被选择) 130 | self.编辑框路径 = 单行编辑框(self.ui.lineEdit_lujing) 131 | self.按钮检查更新 = 按钮(self.ui.pushButton_jianchagengxin) 132 | self.按钮检查更新.绑定事件被点击(self.按钮检查更新被点击) 133 | self.选择框自动播放 = 复选框(self.ui.checkBox) 134 | self.选择框自动播放.选中 = True 135 | self.播放设备 = None 136 | self.检查更新窗口 = None 137 | 138 | # 139 | self.刷新设备 = 刷新设备线程(self.刷新设备线程回调函数) 140 | self.按钮刷新被点击() 141 | 142 | self.文件服务器 = 文件服务器线程() 143 | self.文件服务器.start() 144 | 145 | # self.编辑框路径.内容 = "/Users/chensuilong/Documents/lzxd/廉政行动2022.2022.EP05.HD1080P.X264.AAC.Cantonese.CHS.BDYS.mp4" 146 | # 注册文件拖放 绑定事件 147 | self.ui.centralwidget.setAcceptDrops(True) 148 | self.ui.centralwidget.dragEnterEvent = self.拖放事件 149 | # 拖放结束事件 150 | self.ui.centralwidget.dropEvent = self.拖放结束事件 151 | 152 | def 按钮检查更新被点击(self): 153 | if self.检查更新窗口 is None: 154 | self.检查更新窗口 = 自动更新模块.窗口_更新软件(Github项目名称=全局_项目名称, 155 | 应用名称=全局_应用名称, 156 | 当前版本号=全局_当前版本, 157 | 官方网址=全局_官方网址) 158 | self.检查更新窗口.show() 159 | 160 | def 注册托盘图标(self): 161 | self.托盘图标 = QSystemTrayIcon(self) 162 | icon = QIcon() 163 | icon.addFile(u":/app/app.png", QSize(), QIcon.Normal, QIcon.Off) 164 | 165 | self.托盘图标.setIcon(icon) 166 | self.托盘图标.setToolTip("多多投屏 点击隐藏或显示") 167 | self.托盘图标.activated.connect(self.托盘图标被点击) 168 | self.托盘图标.show() 169 | 170 | def 托盘图标被点击(self, reason): 171 | # 图标位置 = self.托盘图标.geometry() 172 | # self.move(图标位置.x(), 图标位置.y()) 173 | # 点击显示或隐藏窗口 174 | if self.isVisible(): 175 | self.hide() 176 | else: 177 | self.show() 178 | 179 | def 拖放结束事件(self, event): 180 | # 发送点击消息 181 | 182 | if self.选择框自动播放.选中: 183 | self.按钮开始播放.对象.click() 184 | 185 | def 拖放事件(self, event): 186 | if event.mimeData().hasUrls(): 187 | print("拖放事件") 188 | # 获取拖放文件的路径 189 | 文件路径 = event.mimeData().text() 190 | # 替换文本 file:/// 191 | if 系统_是否为window系统(): 192 | 文件路径 = 文件路径.replace("file:///", "") 193 | else: 194 | 文件路径 = 文件路径.replace("file://", "") 195 | 196 | self.编辑框路径.内容 = 文件路径 197 | 198 | event.accept() 199 | else: 200 | event.ignore() 201 | 202 | def 刷新设备线程回调函数(self, 设备列表): 203 | self.按钮刷新.禁用 = False 204 | self.按钮刷新.标题 = "刷新" 205 | 206 | self.设备列表 = 设备列表 207 | self.组合框设备列表.清空() 208 | for x in 设备列表: 209 | self.当前选中设备URL = self.设备列表[0]['URL'] 210 | Model, URL = x['Model'], x['URL'] 211 | ic(Model, URL) 212 | self.组合框设备列表.添加项目(Model) 213 | 214 | def 按钮刷新被点击(self): 215 | self.按钮刷新.禁用 = True 216 | self.按钮刷新.标题 = "刷新中..." 217 | self.刷新设备.start() 218 | 219 | def 按钮选择文件被点击(self): 220 | print("按钮选择文件被点击") 221 | 文件名 = QFileDialog.getOpenFileName(self, "选择文件", "./", "All Files (*)") 222 | if 文件名[0]: 223 | self.编辑框路径.内容 = 文件名[0] 224 | 225 | def 标签状态条被点击(self, e): 226 | # 设置剪切板文本 227 | QApplication.clipboard().setText(self.标签状态条.标题) 228 | # 提示用户 229 | QMessageBox.information(self, "提示", "已复制到剪切板") 230 | 231 | def 投屏回调函数(self, 播放设备, 播放地址): 232 | self.播放设备 = 播放设备 233 | self.播放地址 = 播放地址 234 | self.标签状态条.标题 = 播放地址 235 | 236 | def 按钮开始播放被点击(self): 237 | print("按钮开始播放被点击") 238 | if self.当前选中设备URL == None: 239 | QMessageBox.warning(self, "提示", "请选择设备") 240 | return 241 | self.投屏 = 投屏线程(self.当前选中设备URL, self.编辑框路径.内容, self.投屏回调函数) 242 | self.投屏.start() 243 | 244 | def 按钮停止播放被点击(self): 245 | print("按钮停止播放被点击") 246 | 投屏模块.停止播放(self.播放设备) 247 | 248 | def 组合框项目被选择(self, 索引): 249 | print("组合框项目被选择", 索引) 250 | self.当前选中设备 = self.设备列表[索引] 251 | ic(self.当前选中设备) 252 | self.当前选中设备URL = self.设备列表[索引]['URL'] 253 | ic(self.当前选中设备URL) 254 | 255 | def closeEvent(self, event: QCloseEvent) -> None: 256 | print("closeEvent") 257 | # 提示用户是否退出 258 | 提示 = self.消息框("程序关闭将无法继续播放 \n是 关闭软件 \n否 保留在托盘图标","提示",4, ["是","否"]) 259 | if 提示 == "否": 260 | self.hide() 261 | event.ignore() 262 | return 263 | 264 | if self.播放设备: 265 | 投屏模块.停止播放(self.播放设备) 266 | event.accept() 267 | 268 | 269 | if __name__ == '__main__': 270 | 自动更新模块.初始化() 271 | 设置语言为中文() 272 | app = QApplication() 273 | window = MainWin() 274 | 275 | sys.exit(app.exec()) 276 | --------------------------------------------------------------------------------