懒人记时 代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

418 lines
13 KiB

using AppTime.Properties;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.SQLite;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AppTime
{
class Recorder
{
public const string ExName = "mkv";
public Recorder()
{
BuildDataPath();
}
/// <summary>
/// 统计周期。
/// </summary>
public int IntervalMs = 1000;
public void Start()
{
new Thread(RecorderThreadProc) { IsBackground = true }.Start();
}
class App
{
public string WinText;
public string AppProcess;
public DateTime TimeStart;
public long WinId;
}
public void BuildDataPath()
{
Directory.CreateDirectory(IconPath);
Directory.CreateDirectory(ScreenPath);
}
Dictionary<int, Process> processes;
public Process GetProcess(int processID)
{
if (processes != null && processes.TryGetValue(processID, out var p))
{
return p;
}
processes = Process.GetProcesses().ToDictionary(p => p.Id);
return processes[processID];
}
class app
{
public long id;
public string process;
public Dictionary<string, win> wins = new Dictionary<string, win>();
}
class win
{
public long id;
public string text;
}
long nextAppId = 0;
Icon GetIcon(string fileName, bool largeIcon)
{
var shfi = new SHFILEINFO();
WinApi.SHGetFileInfo(fileName, 0, ref shfi,
(uint)Marshal.SizeOf(shfi),
(uint)FileInfoFlags.SHGFI_ICON | (uint)FileInfoFlags.SHGFI_USEFILEATTRIBUTES
| (uint)(largeIcon ? FileInfoFlags.SHGFI_LARGEICON : FileInfoFlags.SHGFI_SMALLICON)
);
return Icon.FromHandle(shfi.hIcon);
}
void SaveIcon(Icon icon, string filename)
{
using var img = icon.ToBitmap();
img.Save(filename);
}
public string GetIconPath(long appId, bool large)
{
return Path.Combine(IconPath, $"{appId}{(large ? "l" : "s")}.png");
}
Dictionary<string, app> apps = new Dictionary<string, app>();
app GetApp(Process process)
{
var name = process.ProcessName;
if (apps.TryGetValue(name, out var app))
{
return app;
}
var data = db.ExecuteDynamic(
@"select id from app where process = @process",
new SQLiteParameter("process", name)
).FirstOrDefault();
if (data == null)
{
if (nextAppId == 0)
{
nextAppId = (int)(long)db.ExecuteData("select ifnull(max(id),0) + 1 from app")[0][0];
}
var text = "";
try
{
text = process.MainModule.FileVersionInfo.FileDescription;
using var iconl = GetIcon(process.MainModule.FileName, true);
SaveIcon(iconl, GetIconPath(nextAppId, true));
using var icons = GetIcon(process.MainModule.FileName, false);
SaveIcon(icons, GetIconPath(nextAppId, false));
}
catch (Win32Exception)
{
//ignore
}
catch (FileNotFoundException)
{
//ignore
}
if (string.IsNullOrWhiteSpace(text))
{
text = process.ProcessName;
}
db.Execute(
"insert into app (id, process, text, tagId) values(@id, @process, @text, 0)",
new SQLiteParameter("id", nextAppId),
new SQLiteParameter("process", name),
new SQLiteParameter("text", text)
);
app = new app { id = nextAppId, process = name };
nextAppId++;
}
else
{
app = new app
{
id = data.id,
process = name
};
}
apps.Add(name, app);
//fix icons
var largeIconPath = GetIconPath(app.id, true);
if (!File.Exists(largeIconPath))
{
try
{
using var iconl = GetIcon(process.MainModule.FileName, true);
SaveIcon(iconl, largeIconPath);
using var icons = GetIcon(process.MainModule.FileName, false);
SaveIcon(icons, GetIconPath(app.id, false));
}
catch(Win32Exception)
{
}
}
return app;
}
long nextWinId = 0;
win GetWin(Process process, string winText)
{
var app = GetApp(process);
if (app.wins.TryGetValue(winText, out var win))
{
return win;
}
var data = db.ExecuteDynamic(
"select id from win where appid=@appid and text=@winText",
new SQLiteParameter("appid", app.id),
new SQLiteParameter("winText", winText)
).FirstOrDefault();
if (data == null)
{
if (nextWinId == 0)
{
nextWinId = (int)(long)db.ExecuteData("select ifnull(max(id),0) + 1 from win")[0][0];
}
db.Execute(
"insert into win (id, appId, text) values(@id, @appId, @text)",
new SQLiteParameter("id", nextWinId),
new SQLiteParameter("appId", app.id),
new SQLiteParameter("text", winText)
);
win = new win { id = nextWinId, text = winText };
nextWinId++;
}
else
{
win = new win
{
id = data.id,
text = winText
};
}
app.wins.Add(winText, win);
return win;
}
DB db = DB.Instance;
public void RecorderThreadProc()
{
App lastApp = null;
while (true)
{
var now = DateTime.Now;
var hwnd = WinApi.GetForegroundWindow();
var text = new StringBuilder(255);
WinApi.GetWindowText(hwnd, text, 255);
var winText = text.ToString();
WinApi.GetWindowThreadProcessId(hwnd, out var processid);
var process = GetProcess(processid);
var appname = process.ProcessName;
if (lastApp != null)
{
db.Execute(
"update period set timeend = @v1 where timestart = @v0",
lastApp.TimeStart,
now.AddMilliseconds(-1)//必须减小,否则可能与下个周期开始时间重叠
);
}
if (lastApp == null || lastApp.AppProcess != appname || lastApp.WinText != winText)
{
var win = GetWin(process, winText);
lastApp = new App { WinId = win.id, AppProcess = appname, TimeStart = now, WinText = winText };
db.Execute(
"insert into [period](winid, timeStart, timeEnd) values(@v0, @v1, @v1)",
win.id, now
);
}
Screenshot(now);
//等到下一个周期
var nextTime = now.AddMilliseconds(IntervalMs);
now = DateTime.Now;
if(nextTime > now)
{
Thread.Sleep(nextTime - now);
}
}
}
string DataPath => string.IsNullOrWhiteSpace(Settings.Default.DataPath) ? Application.StartupPath : Settings.Default.DataPath;
public string ScreenPath => Path.Combine(DataPath, "images");
public string IconPath => Path.Combine(DataPath, "icons");
ImageCodecInfo jpgcodec = ImageCodecInfo.GetImageDecoders().First(codec => codec.MimeType == "image/jpeg");
///// <summary>
///// 获取图片文件路径
///// </summary>
///// <param name="timeStart"></param>
///// <param name="timeImage"></param>
///// <returns></returns>
//public string getImageFile(DateTime timeStart, DateTime timeImage)
//{
// var folder = Path.Combine(ScreenPath, timeImage.ToString("yyyyMMdd"));
// var filename = $"{timeStart:HHmmss}+{Math.Round((timeImage - timeStart).TotalSeconds)}";
// return Path.Combine(folder, $"{filename}.jpg");
//}
public string getFileName(DateTime time)
{
return Path.Combine(ScreenPath, $"{time:yyyyMMdd}", $"{time:HHmmss}." + Recorder.ExName);
}
DateTime lastCheck = DateTime.MinValue.Date;
/// <summary>
/// 截图
/// </summary>
/// <param name="now"></param>
/// <param name="lastApp"></param>
void Screenshot(DateTime now)
{
//检查记录天数限制
if (lastCheck != now.Date)
{
var firstDate = now.Date.AddDays(-Settings.Default.RecordScreenDays);
var dirs = Directory.EnumerateDirectories(ScreenPath, "????????");
foreach (var i in dirs)
{
if (DateTime.TryParseExact(Path.GetFileName(i), "yyyyMMdd", CultureInfo.CurrentCulture, DateTimeStyles.None, out var date))
{
if (date < firstDate)
{
Directory.Delete(i);
}
}
}
lastCheck = now.Date;
}
if (Settings.Default.RecordScreenDays == 0)
{
return;
}
if (buffer == null)
{
buffer = new MemoryBuffer(now);
}
using var img = GetScreen();
using var mem = new MemoryStream();
img.Save(mem, ImageFormat.Jpeg);
buffer.Frames.Add(new Frame(now - buffer.StartTime, mem.ToArray()));
if ((now - buffer.StartTime).TotalSeconds >= 10 * 60 || now.Date != buffer.StartTime.Date) //固定为10mins,防止保存时间长,减少出问题时影响的时长。
{
FlushScreenBuffer();
}
}
public void FlushScreenBuffer()
{
//切换到新buffer
var newBuffer = new MemoryBuffer(DateTime.Now);
var b = buffer;
buffer = newBuffer;
//加入flushing
lock (flushing)
{
flushing.Add(b);
}
var path = getFileName(b.StartTime);
var folder = Path.GetDirectoryName(path);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
new Thread(() =>
{
Ffmpeg.Save(getFileName(b.StartTime), b.Frames.ToArray());
lock (flushing)
{
flushing.Remove(b);
}
})
{
Priority = ThreadPriority.BelowNormal,
IsBackground = false
}.Start();
}
public class MemoryBuffer
{
public readonly DateTime StartTime;
public readonly List<Frame> Frames = new List<Frame>();
public MemoryBuffer(DateTime startTime)
{
StartTime = startTime;
}
}
public MemoryBuffer buffer;
public List<MemoryBuffer> flushing = new List<MemoryBuffer>();
Bitmap GetScreen()
{
var result = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
using var g = Graphics.FromImage(result);
retry:
try
{
g.CopyFromScreen(0, 0, 0, 0, Screen.PrimaryScreen.Bounds.Size);
}
catch
{
goto retry;
}
return result;
}
}
}