需求背景

尽管现在用手机看片更方便,但无奈下片还是用的电脑,而且手机也不可能存个几百个G的片吧,所以有好几次都试图找一个简单的用手机看电脑上的视频的方法,试过共享文件夹,ES浏览器,windows自带的mediaplayer之类的方案。虽然最后勉强能看片了,但是一方面,我觉得设置起来很麻烦,另一方面,这些方案都不支持外接的ASS/SSA/SRT字幕播放。

因此,在无字幕强行啃生肉多次之后。。我终于决定自己实现一个能带字幕播放媒体文件的文件浏览器。

最终实现

一个本机运行的文件服务器(由python脚本打包而成的EXE可执行程序) + 通过手机浏览器查看。
支持自动加载视频文件同名SRT/SSA/ASS字幕。
支持扫二维码快速访问。

优势

简单(双击打开就能运行,不用任何设置) 自动加载字幕
另外这个文件浏览器也顺便让不同设备传文件变的简单了,以前我还得开个微信文件传输助手传来传去,速度慢还有100M的大小限制

实现流程

首先确定网络服务器框架代码,这里我使用python的flask框架。如果是视频文件,跳转播放页;如果是文件夹,则显示文件列表;如果是普通文件,则直接提供下载。

然后是字幕转换问题,由于原生的html字幕只支持vtt格式,所以我需要把ASS、SRT字幕都转成vtt格式,试验了一些三方依赖之后我选择了 webvtt和asstosrt 这两个库。后者支持ASS和SSA字幕转SRT。

最后,给出源代码及封装好的EXE文件地址:源码及EXE下载地址(github)

附python源码,写的很丑陋,仅供参考。如果有python环境的可以安装相关依赖后从脚本运行。

# -*- coding: utf-8 -*-

from flask import Flask, send_from_directory, send_file
from gevent import monkey
from gevent.pywsgi import WSGIServer
import os
import urllib
import sys
import html
import os
import webvtt
import asstosrt
import chardet
from tkinter import *
from tkinter import filedialog
import locale
import time

monkey.patch_all()
from flask_cors import *

app = Flask(__name__)
CORS(app, supports_credentials=True)

WORK_DIRECTORY = '/'
SEP = '/'
lang = locale.getdefaultlocale()[0]
IS_ZH = 'CN' in lang
BACK = "返回首页" if IS_ZH else 'back to index'
NAME = '仔仔文件传输助手' if IS_ZH else "ZZ file explorer"


@app.route('/')
def hello_world():
    return get_list_page(WORK_DIRECTORY)


@app.route("/static/<path:path>", methods=["GET"])
def static_dir(path):
    return send_from_directory('./static/', path)


@app.route("/file/<path:path>", methods=["GET"])
def get_file(path):
    print('getfile', path)
    suffix = path.split(".")
    suffix = suffix[-1] if suffix else ""
    path = urllib.parse.unquote(path)
    path = path.replace("@@", SEP)
    print(path)
    if path.endswith(SEP):  # 判断是否是文件夹
        return get_list_page(path)
    elif suffix not in ["exe", "bat"]:
        path = path.replace("@@", SEP)
        return send_file(path, conditional=True)


@app.route("/play/<path:path>", methods=["GET"])
def get_video_player(path):
    # show the user profile for that user
    print(path)
    r = []
    try:
        displaypath = urllib.parse.unquote(path,
                                           errors='surrogatepass')
    except UnicodeDecodeError:
        displaypath = urllib.parse.unquote(path)

    displaypath = html.escape(displaypath, quote=False)
    enc = sys.getfilesystemencoding()
    title = 'Player for %s' % displaypath
    r.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
             '"http://www.w3.org/TR/html4/strict.dtd">')
    r.append('<html>\n<head>')

    r.append('<meta http-equiv="Content-Type" '
             'content="text/html; charset=%s">' % enc)
    r.append('<title>%s</title>\n</head>' % title)
    # r.append('<script src="/static/player.js"></script>'
    #          # '<script src="/static/videosub-0.9.9.js"></script>'
    #          )
    r.append('<body>\n<h1>%s</h1>' % title)
    r.append('<li><a href="%s">%s</a></li>'
             % ('/',
                html.escape(BACK, quote=False)))
    r.append(
        '\n<p align="center"><video id="mainPlayer"   controls="controls" width="640" height="480">')
    caption_path = getCaption(path.replace('@@', SEP)).replace(SEP, '@@')
    r.append('''
    <source src="/file/%s" type="video/mp4">
     <track id="video-caption" src="/file/%s"  
     kind="captions" srclang="zh" label="Chinese" default/>
    ''' % (path, caption_path))
    r.append('</video></p>\n\n')
    r.append('</body>\n</html>\n')
    return '\n'.join(r)


def get_list_page(path):
    """Helper to produce a directory listing (absent index.html).

    Return value is either a file object, or None (indicating an
    error).  In either case, the headers are sent, making the
    interface the same as for send_head().

    """

    try:
        list = os.listdir(path)
    except OSError as e:
        print(e)
        return None
    list.sort(key=lambda a: a.lower())
    r = []
    displaypath = html.escape(path, quote=False)
    enc = sys.getfilesystemencoding()
    title = NAME + ' Directory listing for %s' % displaypath
    r.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
             '"http://www.w3.org/TR/html4/strict.dtd">')
    r.append('<html>\n<head>')
    r.append('<meta http-equiv="Content-Type" '
             'content="text/html; charset=%s">' % enc)
    r.append('<title>%s</title>\n</head>' % title)
    r.append('<body>\n<h1>%s</h1>' % title)
    r.append('<hr>\n<ul>')
    if path != WORK_DIRECTORY:  # 'a/b/c/'  -> 'a/b/'
        r.append('<li><a href="%s">%s</a></li>'
                 % ('/',
                    html.escape(BACK, quote=False)))

    for name in list:
        fullname = os.path.join(path, name)
        displayname = linkname = name
        # print(name, fullname)
        # Append / for directories
        if os.path.isdir(fullname):
            fullname = fullname + "/"
            displayname = name + "/"
            linkname = name + "/"

        suffix = linkname.split('.')[-1]
        if suffix in ['mp4', 'avi', 'mkv']:
            r.append('<li><a href="%s">%s</a></li>'
                     % ('/play/' + urllib.parse.quote(fullname.replace(SEP, "@@")),
                        html.escape(displayname, quote=False)))
        else:
            r.append('<li><a href="%s">%s</a></li>'
                     % ('/file/' + urllib.parse.quote(fullname.replace(SEP, "@@")),
                        html.escape(displayname, quote=False)))
    r.append('</ul>\n<hr>\n</body>\n</html>\n')
    return '\n'.join(r)


def getCaption(videoPath):
    # 根据video名称寻找字幕
    print('get caption',videoPath)
    vtt_exist, srt_exist, ass_exist,ssa_exist = False, False, False,False
    dir_path = os.path.dirname(videoPath)
    videoPath = videoPath.split('/')[-1]
    videoName = videoPath.split('.')[0]
    print(videoName)
    vtt_name = videoName + '.vtt'
    srt_name = videoName + '.srt'
    ass_name = videoName + '.ass'
    ssa_name = videoName + '.ssa'
    file_list = os.listdir(dir_path)
    for file_name in file_list:
        # print(file_name)
        if file_name == vtt_name:
            vtt_exist = True
        elif file_name == srt_name:
            srt_exist = True
        elif file_name == ass_name:
            ass_exist = True
        elif file_name == ssa_name:
            ssa_exist = True
    if vtt_exist:
        return os.path.join(dir_path, vtt_name)
    elif srt_exist:
        return srt2vtt(srt_name, dir_path)
    elif ass_exist:
        return ass2vtt(videoName, dir_path,ass_name)
    elif ssa_exist:
        return ass2vtt(videoName, dir_path,ssa_name)
    else:
        return ''


def srt2vtt(srt_name, dir_path):
    print('srt2vtt',srt_name)
    srt_path = os.path.join(dir_path, srt_name)
    with  open(file=srt_path, mode='rb')  as f3:  # 以二进制模式读取文件
        data = f3.read()  # 获取文件内容
        # print(data)
        f3.close()  # 关闭文件
        origin_charset = chardet.detect(data)['encoding']  # 检测文件内容
        print(origin_charset)

    convert_webvtt = webvtt.from_srt(srt_path)
    convert_webvtt.save()
    return convert_webvtt.file


def removeIfExists(file_path):
    if (os.path.exists(file_path)):
        os.remove(file_path)


def ass2vtt(video_name, dir_path,ass_name):
    # 编码转换 然后用pysub
    ass_path = os.path.join(dir_path, ass_name)
    print(ass_path)
    with  open(file=ass_path, mode='rb')  as f3:  # 以二进制模式读取文件
        data = f3.read()  # 获取文件内容
        # print(data)
        f3.close()  # 关闭文件
        origin_charset = chardet.detect(data)['encoding']  # 检测文件内容
        print(origin_charset)
    tmp_path = ass_path
    tmp_ass_path = os.path.join(dir_path, 'tmp.ass')
    if origin_charset != 'utf8':
        tmp_path = tmp_ass_path
        with open(tmp_path, 'wb') as tmp_file:
            data = data.decode(origin_charset).encode('utf8')
            tmp_file.write(data)
    with open(tmp_path,encoding='utf8') as f:
        srt_str = asstosrt.convert(f)
        print(srt_str)
    removeIfExists(tmp_ass_path)
    tmp_srt_path = os.path.join(dir_path, video_name + '.srt')
    with open(tmp_srt_path, 'w',newline="",encoding="utf8") as f:
        f.write(srt_str)
    return srt2vtt(tmp_srt_path, dir_path)


def get_ip():
    import socket
    ip = '本机ip'
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(('8.8.8.8', 80))
        ip = s.getsockname()[0]
    finally:
        s.close()
    print(ip)
    return ip


def show_qcode(url):
    import qrcode
    qr = qrcode.QRCode(version=2, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=10, )
    qr.add_data(url)
    qr.make(fit=True)
    img = qr.make_image()
    img.show()


if __name__ == '__main__':
    # 或者app.debug = True #代码修改了就自动运行
    # app.run(host='0.0.0.0',port=30006,debug=True)
    try:
        if IS_ZH:
            print("请在弹出框里选择需要共享的目录")
        else:
            print('please select directory to share')
        window = Tk()
        window.withdraw()  # 主窗口隐藏
        WORK_DIRECTORY = filedialog.askdirectory(parent=window, initialdir="/",
                                                 title='choose the share directory')
        if not WORK_DIRECTORY:
            print('cancel')
            sys.exit(1)
        url = "http://%s:30007" % get_ip()
        print("共享目录 " if IS_ZH else 'share directory: ', WORK_DIRECTORY)
        print("服务器启动" if IS_ZH else 'server started  ')
        print(
            '用手机或电脑浏览器访问 %s (或扫描生成的二维码)即可访问共享目录内容' % url if IS_ZH else
            'open link %s with browser on PC/mobile (or just scan the QR code),to xisit your content' % url)
        show_qcode(url)
        WSGIServer(('0.0.0.0', 30007), app).serve_forever()
    except Exception as e:
        print(e)
        time.sleep(10)