用AutoHotkey一键打开当前文件夹上次看过的视频文件并跳转到时间

场景:

  1. 比如文件夹里有序号为1-100的视频,今天看完后,下次可能会不知道上次看到哪集了,更不用说播放的时间
  2. 比如文件夹里有序号为1-100PPT文件,今天看到第3个文件的第10页,下次想接着看(PDFword等同理)

什么时候记录文件名和时间

以下描述只说视频的情况(假设视频用PotPlayer软件播放)

  • 在关闭软件前,按F9调用脚本记录文件名和当前播放时间(依赖记性,所以更推荐下面方法)
  • 养成用脚本命令(F12)关闭PotPlayer的习惯:脚本会在关闭软件前自动检查当前目录有没有记录过文件名(第一次必须手工记录),
    • 如果有,则弹框提示是否保存记录,选则会记录文件名和当前时间。
    • 如果没有,则直接关闭PotPlayer

记录保存在哪里

_SS.fpYaml 定义的文件路径,用是的yaml格式
fn记录文件夹下的文件名
video记录视频文件名和时间(由于各电脑文件不一定在同一目录,所以只记录文件名而不是文件路径)
ppt记录ppt文件名和页号

fn:
  "d:\\视频": 4.mp4
  "d:\\ppt": 3.pptx
ppt: 
  3.pptx: 22
video: 
  4.mp4: 11868

恢复上次查看的文件

  1. TC(资源管理器同理)进入到视频文件夹
  2. F11会从配置文件获取上次查看的文件名并打开,等文件打开后,获取上次记录的时间,直接跳转到相应时间点

附上 AutoHotkey v2-beta 代码
请使用链接里的AutoHotkey64.exe,打包了部分基础库以解决环境问题,比如解析yaml配置文件的库


#hotif WinActive("ahk_class PotPlayer64")
F9::_SS.potSave(1) ;保存PotPlayer当前文件名称和时间
F10::_SS.potRestore(1) ;PotPlayer 当前播放文件跳转到记录的时间
F12::_Pot.smartClose() ;关闭时智能提醒是否记录

#hotif WinActive("ahk_class PPTFrameClass")
F9::_SS.pptWrite() ;记录页号
F10::_SS.pptRestore() ;回到页号

#hotif WinActive("ahk_class TTOTAL_CMD")
;推荐:在TC界面,进入目录后,按F11实现:打开当前目录上次查看的文件并自动跳转到对应时间
F11:: {
    dir := _TC._dir(0) ;用自己的方式获取当前目录
    fnSession := _SS.getFileByDir(dir) ;通过目录读取配置文件中的文件名
    if (fnSession != "")
        _TC.gotoLine(fnSession, 0, 3)
}
#hotif

;记录/恢复 pdf|视频等文件的进度
class _SS {

    static fpYaml := "d:\BB\ini\session.yaml"
    static readYaml() => yaml.parse(fileread(this.fpYaml, "utf-8"))

    static writeYaml(objYaml) {
        FileDelete(this.fpYaml)
        sFile := yaml.stringify(objYaml)
        FileAppend(sFile, this.fpYaml, "utf-8")
    }

    ;记录当前目录,上次看的哪个文件
    static write(k1, k2, v2) {
        objYaml := this.readYaml()
        if (objYaml.has(k1)) {
            objYaml[k1][k2] := v2
            OutputDebug(format("i#{1} {2}:{3} {4}={5}", A_LineFile,A_LineNumber,k1,k2,v2))
            this.writeYaml(objYaml)
        }
    }
    static read(k1, k2) {
        objYaml := this.readYaml()
        if (objYaml[k1].has(k2)) {
            val := objYaml[k1][k2]
            OutputDebug(format("i#{1} {2}:val={3}", A_LineFile,A_LineNumber,val))
            return val
        } else {
            OutputDebug(format("i#{1} {2}:{3} not found", A_LineFile,A_LineNumber,k2))
        }
    }

    static getFileByDir(dir) {
        objYaml := this.readYaml()
        fn := objYaml["fn"].get(dir, "")
        return fn
    }
    ;记录当前目录,上次看的哪个文件
    static setFileByDir(fp, dir:="") {
        OutputDebug(format("i#{1} {2}:A_ThisFunc={3} fp={4}", A_LineFile,A_LineNumber,A_ThisFunc,fp))
        if (dir == "") {
            SplitPath(fp, &fn, &dir)
        } else {
            fn := fp
        }
        this.write("fn", dir, fn)
    }

    ;打开文件并定位到记录位置
    ;打开文件可能由 tc 执行,这里只负责等待文件打开,再跳转
    static runEx(fp, bRun:=false) {
        SplitPath(fp, &fn, &dir, &ext, &noExt)
        if (bRun)
            run(fp)
        WinWaitActive(fn) ;TODO 完善
        OutputDebug(format("i#{1} {2}:actived", A_LineFile,A_LineNumber))
        switch ext, false {
            case "ppt","pptx":
                _SS.pptRestore()
            case "mp4","flv":
                _SS.potRestore(fp)
        }
    }

    ;PotPlayer
    static potSave(bSetFile:=false) {
        fp := _Pot.currentFile()
        SplitPath(fp, &fn)
        val := _Pot.currentMs()
        this.write("video", fn, val)
        if (bSetFile)
            this.setFileByDir(fp)
        msgbox("已完成",, "0x40000 T1")
    }
    static potRestore(fp:="") {
        endTime := A_TickCount + 5000
        loop {
            OutputDebug(format("i#{1} {2}:_Pot._status()={3}", A_LineFile,A_LineNumber,_Pot._status()))
            if (_Pot.isPlaying())
                break
            else
                sleep(200)
        } until (A_TickCount >= endTime)
        OutputDebug(format("i#{1} {2}:ok", A_LineFile,A_LineNumber))
        if (fp == "")
            fp := _Pot.currentFile()
        SplitPath(fp, &fn)
        _Pot.setTime(this.read("video", fn))
    }

    ;ppt
    static pptWrite() {
        ppt := ComObjActive("Powerpoint.application")
        fn := ppt.ActivePresentation.name
        idx := ppt.ActiveWindow.view.slide.SlideNumber
        this.write("ppt",fn, idx)
    }
    static pptRestore() {
        ppt := getPPT()
        i := this.read("ppt", ppt.ActivePresentation.name)
        if (i) {
            OutputDebug(format("i#{1} {2}:page={3}", A_LineFile,A_LineNumber,i))
            ppt.ActiveWindow.view.GotoSlide(i)
            ;ppt.ActivePresentation.slides(i).select
        }
        getPPT() {
            endTime := A_TickCount + 10000
            loop {
                try {
                    ppt := ComObjActive("Powerpoint.application")
                    tooltip
                    return ppt
                } catch {
                    tooltip(A_Index)
                    sleep(500)
                }
            } until (A_TickCount >= endTime)
        }
    }

}

class _TC {
    static path := "d:\TC\TOTALCMD64.exe"

    ;若TC已打开 用A_DetectHiddenWindows 解决
    ;保证TC打开(顺带激活)
    static um_Ready(bActive:=false) {
        if !WinExist("ahk_class `TTOTAL_CMD`") {
            saveDetect := A_DetectHiddenWindows
            DetectHiddenWindows(true)
            if (WinExist("ahk_class TTOTAL_CMD")) {
                WinShow("ahk_class TTOTAL_CMD")
            } else {
                run(_TC.path)
                WinWaitActive("ahk_class TTOTAL_CMD")
            }
            DetectHiddenWindows(saveDetect)
        }
        if (bActive)
            WinActivate("ahk_class TTOTAL_CMD")
    }

    static smartNum(tp:=0) => (_TC.get(1000)==1) ? tp : !tp
    static get(n) {
        _TC.um_Ready()
        return SendMessage(1074, n,,, "ahk_class TTOTAL_CMD") ;NOTE 核心接口函数
    }
    static post(n, param:=0) {
        if (n == 0)
            return
        try { ;WinHide时会失败
            PostMessage(1075, n, param,, "ahk_class TTOTAL_CMD")
        } catch {
            WinShow("ahk_class TTOTAL_CMD")
            PostMessage(1075, n,,, "ahk_class TTOTAL_CMD")
        }
    }
    static _dir(tp:=0) => RegExReplace(ControlGetText(_TC._ctlDir(tp), "ahk_class TTOTAL_CMD"), "[/\\][^/\\]+$") ;来源:目录路径(代替C2) NOTE 如果对面是快速预览,则控件内容是文件名,_dir为空
    static _ctlDir(tp:=0) => _TC.get(9+_TC.smartNum(tp)) ;来源:路径控件 左9/右10

    static gotoLine(i, tp:=0, bFocus:=1) {
        if (i is string) {
            fn := i
            i := _TC._getIndexByFn(fn,tp)
            if (!i) {
                OutputDebug(format("i#{1} {2}:file not found={3}", A_LineFile,A_LineNumber,fn))
                return 0
            }
        }
        OutputDebug(format("i#{1} {2}:file index={3}", A_LineFile,A_LineNumber,i))
        if (bFocus && i is integer && i) {
            PostMessage(0x19E, i-1, 1, _TC.get(tp+3), "ahk_class TTOTAL_CMD")
            if (bFocus >= 2) {
                _TC.post(1001)
                if (bFocus >= 3)
                    _SS.runEx(fn)
            }
        }
        return i
    }

    ;第1行..为1
    static _getIndexByFn(fn, tp:=0) { ;来源:获取文件名所在的序号(定位用)
        loop(2) {
            arr := _TC.arrLines(tp)
            ;OutputDebug(format("i#{1} {2}:arr={3}", A_LineFile,A_LineNumber,json.stringify(arr,4)))
            for k, v in arr {
                ;OutputDebug(format("i#{1} {2}:v={3},fn={4}", A_LineFile,A_LineNumber,v,fn))
                if (v = fn)
                    return k ;第一行为返回上一级
            }
            tp ? _TC.um_RereadTrg() : _TC.cm_RereadSource()
        }
        ;noExt再判断
        for k, v in _TC.arrNoExt(tp) {
            if (v = fn)
                return k
        }
        return 0
    }

    ;文件所有信息
    ;包含第一行 .. 内容
    ;lineFilter
    ;   0=选中行(NOTE 如果没有选中,以当前行为结果)
    ;   1=光标所在行
    ;如果是展开文件夹,则文件名其实是带文件夹的,不需要文件夹,则指定 noDir
    ;返回数组
    ;NOTE 文件名太长有 .. 不影响获取文件名
    static arrLines(tp:=0, lineFilter:=unset, colFilter:=1) {
        arrAll := ControlGetItems(_TC.get(tp+3), "ahk_class TTOTAL_CMD")
        if (arrAll.length == 1)
            return []
        ;OutputDebug(format("i#{1} {2}:arrAll={3}", A_LineFile,A_LineNumber,json.stringify(arrAll,4)))
        ;第1行 .. 添加空值防止后面判断出错
        if (StrSplit(arrAll[1],A_Tab).length == 1) {
            loop(StrSplit(arrAll[2]).length-1)
                arrAll[1] .= "`t"
        }
        if isset(lineFilter) {
            arrLine := []
            if (isobject(lineFilter)) {
                for sLine in arrAll {
                    arrTmp := StrSplit(sLine,A_Tab)
                    str := lineFilter(arrTmp)
                    if (str != "")
                        arrLine.push(str)
                }
            } else {
                ;没有选中项,则处理当前行
                if (lineFilter==0 && _TC._selectCount(tp)==0)
                    lineFilter := 1
                if (lineFilter == 0) {
                    arrSelect := _TC._arrIdxSelected(tp)
                    ;OutputDebug(format("i#{1} {2}:arrSelect={3}", A_LineFile,A_LineNumber,json.stringify(arrSelect,4)))
                    for i in arrSelect
                        arrLine.push(arrAll[i])
                } else if (lineFilter == 1) {
                    i := this._index(tp)
                    arrLine := [arrAll[i]]
                }
            }
        } else {
            arrLine := arrAll
        }
        ;根据列再次过滤
        if (isset(colFilter)) {
            if (colFilter is integer) {
                colFilter := [colFilter]
            } else if (colFilter is string) {
                ;OutputDebug(format("i#{1} {2}:string={3}", A_LineFile,A_LineNumber,colFilter))
                colFilter := [this._column(arrAll, colFilter)]
                ;OutputDebug(format("i#{1} {2}:colFilter={3}", A_LineFile,A_LineNumber,json.stringify(colFilter,4)))
            } else if (colFilter is array) {
                colFilter := this._column(arrAll, colFilter)
            }
            arrCol := []
            if (colFilter.length == 1) {
                for sLine in arrLine
                    arrCol.push(rtrim(StrSplit(sLine,A_Tab)[colFilter[1]])) ;加了 rtrim 主要解决 <DIR> 右边会有两个空格
            } else { ;比较少,获取多列数据
                for sLine in arrLine {
                    arrTmp := StrSplit(sLine,A_Tab)
                    arrCol.push([]) ;二维数组
                    for i in colFilter
                        try
                            arrCol[-1].push(arrTmp[i])
                    catch
                        msgbox(json.stringify(arrTmp, 4))
                }
            }
            ;if (arrCol is array)
            ;    OutputDebug(format("i#{1} {2}:arrCol={3}", A_LineFile,A_LineNumber,json.stringify(arrCol,4)))
            ;else
            ;    OutputDebug(format("i#{1} {2}:arrCol={3}", A_LineFile,A_LineNumber,arrCol))
            return arrCol
        } else {
            return arrLine
        }
    }

}

; PotPlayer Function Library by Specter333
; A complete list of functions can be found at the bottom of the page.
; Use Send functions to control PotPlayer with out it being active.
class _Pot {

    static currentFile() {
        title := WinGetTitle("ahk_class PotPlayer64")
        if (title == "PotPlayer")
            return ""
        fn := RegExReplace(title, " - PotPlayer")
        arr := hyf_GetOpenedFiles(WinGetPID("ahk_class PotPlayer64"), (fp)=>instr(fp,fn))
        if (!arr.length)
            return
        fp := arr[1]
        if (fp ~= "^UNC")
            fp := RegExReplace(fp, "^UNC", "\")
        return fp
    }

    smartClose() {
        hwnd := WinExist("A") ;先记录 hwnd
        fp := _Pot.currentFile()
        SplitPath(fp,, &dir)
        fn := _SS.getFileByDir(dir)
        if (fn != "") { ;当前文件所在目录已有过记录
          if (msgbox("是否要保存记录",, 0x40004) = "Yes")
            _SS.potSave()
        }
        PostMessage(0x112, 0xF060,, hwnd) ;关闭窗口
    }

    /*
    These commands use 0x111 as the Msg parameter.
    CMD_PLAY              = 20001;
    CMD_PAUSE             = 20000;
    CMD_STOP              = 20002;
    CMD_PREVIOUS          = 10123;
    CMD_NEXT              = 10124;
    CMD_PLAY_PAUSE        = 10014;
    CMD_VOLUME_UP         = 10035;
    CMD_VOLUME_DOWN       = 10036;
    CMD_TOGGLE_MUTE       = 10037;
    CMD_TOGGLE_PLAYLIST   = 10011;
    CMD_TOGGLE_CONTROL    = 10383;
    CMD_OPEN_FILE         = 10158;
    CMD_TOGGLE_SUBS       = 10126;
    CMD_TOGGLE_OSD        = 10351;
    CMD_CAPTURE           = 10224;

    These commands use 0x400 as the Msg parameter
    POT_GET_VOLUME   0x5000 // 0 ~ 100
    POT_SET_VOLUME   0x5001 // 0 ~ 100
    POT_GET_TOTAL_TIME  0x5002 // ms unit
    POT_GET_PROGRESS_TIME 0x5003 // ms unit
    POT_GET_CURRENT_TIME 0x5004 // ms unit
    POT_SET_CURRENT_TIME 0x5005 // ms unit
    POT_GET_PLAY_STATUS  0x5006 // -1:Stopped, 1:Paused, 2:Running
    POT_SET_PLAY_STATUS  0x5007 // 0:Toggle, 1:Paused, 2:Running
    POT_SET_PLAY_ORDER  0x5008 // 0:Prev, 1:Next
    POT_SET_PLAY_CLOSE  0x5009
    POT_SEND_VIRTUAL_KEY 0x5010 // Virtual Key(VK_UP, VK_DOWN....)
    */
    static post(msg) => PostMessage(0x111, msg,,, "ahk_class PotPlayer64")
    static open() => this.post(10158)
    static play() {
        if (!this.isPlaying())
            this.post(20001)
    }
    static pause() {
        if (this.isPlaying())
            this.post(20000)
    }
    static isPlaying() => _Pot._status() == 2
    static _status() => SendMessage(0x400, 0x5006,,, "ahk_class PotPlayer64") ;-1:Stopped, 1:Paused, 2:Running
    static playPause() => this.post(10014)
    static stop() => this.post(20002)
    static previous() => this.post(10123)
    static next() => this.post(10124)
    static back5s() => this.post(10059)
    static forward5s() => this.post(10060)
    static back30s() => this.post(10061)
    static forward30s() => this.post(10062)
    static volumeUp() => this.post(10035)
    static volumeDown() => this.post(10036)
    static mute() => this.post(10037)
    static close() => WinClose("ahk_class PotPlayer64")

    static getVol() => this.post(0x5000)
    static setVol(newvol) => this.post(0x5001, newvol)
    ;当前时间
    static currentMs() => SendMessage(0x400, 0x5004,,, "ahk_class PotPlayer64") ; Retrives the current possition in milliseconds
    static getCurrentTime() => _Pot.timeFromMs(this.currentMs()) ;POT_GET_CURRENT_TIME
    static totalMs() => SendMessage(0x400, 0x5002,,, "ahk_class PotPlayer64") ; Retrives the current possition in milliseconds
    static getTotalTime() => _Pot.timeFromMs(this.totalMs())
    static setTime(ms) => SendMessage(0x400, 0x5005, ms,, "ahk_class PotPlayer64") ; Value in milliseconds
    static playAtTime(tm) {
        this.post(10325) ;g
        WinWaitActive("直接输入移动位置")
        OutputDebug(format("i#{1} {2}:tm={3}", A_LineFile,A_LineNumber,tm))
        sendEx(100, tm . "`n", 100)
        WinClose()
        this.play()
    }

    ;3610590 →01:00:10.590
    ;tp 1=显示毫秒 0=直接删除毫秒 NOTE -1=根据毫秒四舍五入
    static timeFromMs(ms:=0, tp:=0) {
        if !(ms is integer)
            throw TypeError(format("type(ms) = {1}", type(ms)))
        OutputDebug(format("i#{1} {2}:ms={3}", A_LineFile,A_LineNumber,ms))
        if (tp==-1 && mod(ms,1000)>=500)
            ms += 500
        arr := []
        loop 3
            arr.push(floor(mod((ms / (1000 * 60**(3-A_Index))),60)))
        ;OutputDebug(format("i#{1} {2}:arr={3}", A_LineFile,A_LineNumber,json.stringify(arr,4)))
        if (tp == 1) {
            arr.push(mod(ms, 1000))
            return format("{:02}:{:02}:{:02}.{:03}",arr*)
        } else {
            return format("{:02}:{:02}:{:02}",arr*)
        }
    }

    static jumpForward() { ; Set for 5 seconds but seems to jump an arbitrary amount.
        newtime := this.currentMs()+5000 ; Add 5 seconds to current time.
        this.setTime(newtime)
    }
    static jumpBackward() { ; Set for -5 seconds but seems to jump an arbitrary amount.
        newtime := this.currentMs()-5000 ; Subract 5 seconds from current time.
        this.setTime(newtime)
    }

}
posted @ 2023-01-13 19:52  火冷  阅读(673)  评论(0编辑  收藏  举报