用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)
}
}