用AutoHotkey一键打开当前文件夹上次看过的视频文件并跳转到时间
场景:
- 比如文件夹里有序号为
1-100
的视频,今天看完后,下次可能会不知道上次看到哪集了,更不用说播放的时间 - 比如文件夹里有序号为
1-100
的PPT
文件,今天看到第3
个文件的第10
页,下次想接着看(PDF
,word
等同理)
什么时候记录文件名和时间
以下描述只说视频的情况(假设视频用
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
恢复上次查看的文件
TC
(资源管理器同理)进入到视频文件夹- 按
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) } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述