懒人记时 代码仓库
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.

623 line
18 KiB

  1. 
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Data;
  5. using System.Data.SQLite;
  6. using System.Diagnostics;
  7. using System.Drawing;
  8. using System.Drawing.Imaging;
  9. using System.Globalization;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Runtime.InteropServices;
  13. using System.Text;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. using static AppTime.Recorder;
  17. namespace AppTime
  18. {
  19. class Controller
  20. {
  21. public WebServer Server =>Program.server;
  22. public Recorder Recorder=>Program.recorder;
  23. #region basic
  24. int getColor(string str)
  25. {
  26. return str.GetHashCode();
  27. }
  28. static Dictionary<long, int> appColors = new Dictionary<long, int>();
  29. int getAppColor(long appId)
  30. {
  31. if(!appColors.TryGetValue(appId, out var color))
  32. {
  33. var text = db.ExecuteValue<string>($"select text from app where id={appId}");
  34. lock (appColors)
  35. {
  36. color = appColors[appId] = getColor(text);
  37. }
  38. }
  39. return color;
  40. }
  41. static Dictionary<long, int> tagColors = new Dictionary<long, int>();
  42. int getTagColor(long tagId)
  43. {
  44. if (!tagColors.TryGetValue(tagId, out var color))
  45. {
  46. if (tagId == -1)
  47. {
  48. color = tagColors[tagId] = BitConverter.ToInt32(new byte[] { 0xFF, 0x99, 0x99, 0x99 }, 0);
  49. }
  50. else
  51. {
  52. var text = db.ExecuteValue<string>($"select text from tag where id={tagId}");
  53. color = tagColors[tagId] = getColor(text);
  54. }
  55. }
  56. return color;
  57. }
  58. public unsafe byte[] getPeriodBar(DateTime timefrom, DateTime timeto, string view, int width)
  59. {
  60. if (width <= 0 || width > 8000)
  61. {
  62. return null;
  63. }
  64. var totalsecs = (timeto - timefrom).TotalSeconds;
  65. IEnumerable<dynamic> data;
  66. if (view == "app")
  67. {
  68. data = db.ExecuteDynamic(@"
  69. select
  70. app.id appId,
  71. p.timeStart,
  72. p.timeEnd
  73. from app
  74. join win on win.appid = app.id
  75. join period p on p.winid = win.id
  76. where
  77. timeStart between @v0 and @v1
  78. or timeEnd between @v0 and @v1
  79. or @v0 between timeStart and timeEnd
  80. order by p.timeStart
  81. ",
  82. timefrom, timeto
  83. );
  84. }
  85. else
  86. {
  87. data = db.ExecuteDynamic(@"
  88. select
  89. ifnull(tag.id,-1) tagId,
  90. p.timeStart,
  91. p.timeEnd
  92. from app
  93. join win on win.appid = app.id
  94. join period p on p.winid = win.id
  95. left join tag on tag.id = win.tagId or (win.tagId = 0 and tag.id = app.tagId)
  96. where
  97. timeStart between @v0 and @v1
  98. or timeEnd between @v0 and @v1
  99. or @v0 between timeStart and timeEnd
  100. order by p.timeStart
  101. ",
  102. timefrom, timeto
  103. );
  104. }
  105. //绘制PeriodBar,直接写内存比gdi+快
  106. var imgdata = new int[width];
  107. foreach (var period in data)
  108. {
  109. var from = Math.Max(0, (int)Math.Round((period.timeStart - timefrom).TotalSeconds / totalsecs * width));
  110. var to = Math.Min(width - 1, (int)Math.Round((period.timeEnd - timefrom).TotalSeconds / totalsecs * width));
  111. for (var x = from; x <= Math.Min(width - 1, to); x++)
  112. {
  113. imgdata[x] = view == "app" ? (int)getAppColor(period.appId) : (int)getTagColor(period.tagId);
  114. }
  115. }
  116. fixed (int* p = &imgdata[0])
  117. {
  118. var ptr = new IntPtr(p);
  119. using var bmp = new Bitmap(width, 1, width * 4, PixelFormat.Format32bppArgb, ptr);
  120. using var mem = new MemoryStream();
  121. bmp.Save(mem, ImageFormat.Png);
  122. return mem.ToArray();
  123. }
  124. }
  125. public object getTree(DateTime timefrom, DateTime timeto, string view, long parentKey)
  126. {
  127. var result = new List<object>();
  128. var totalSeconds = (timeto - timefrom).TotalSeconds;
  129. IEnumerable<dynamic> data;
  130. if (view == "app")
  131. {
  132. if (parentKey == 0)
  133. {
  134. data = db.ExecuteDynamic(@"
  135. select
  136. app.id appId,
  137. app.text appText,
  138. tag.text tagText,
  139. sum(julianday(case when timeEnd > @v1 then @v1 else timeEnd end) -
  140. julianday(case when timeStart < @v0 then @v0 else timeStart end)) days
  141. from app
  142. join win on win.appid = app.id
  143. join period p on p.winid = win.id
  144. left join tag on tag.id = app.tagId
  145. where
  146. timeStart between @v0 and @v1
  147. or timeEnd between @v0 and @v1
  148. or @v0 between timeStart and timeEnd
  149. group by app.id
  150. order by days desc
  151. ",
  152. timefrom, timeto
  153. );
  154. foreach (var i in data)
  155. {
  156. var time = new TimeSpan((long)(i.days * TimeSpan.TicksPerDay));
  157. result.Add(
  158. new
  159. {
  160. i.appId,
  161. i.tagText,
  162. text = i.appText,
  163. time = time.ToString(@"hh\:mm\:ss"),
  164. percent = Math.Round(time.TotalSeconds * 100 / totalSeconds, 2) + "%",
  165. children = new object[0]
  166. }
  167. );
  168. }
  169. return result;
  170. }
  171. data = db.ExecuteDynamic(@"
  172. select
  173. win.id winId,
  174. win.text winText,
  175. tag.text tagText,
  176. sum(julianday(case when timeEnd > @v1 then @v1 else timeEnd end) -
  177. julianday(case when timeStart < @v0 then @v0 else timeStart end)) days
  178. from win
  179. join period p on p.winid = win.id
  180. left join tag on tag.id = win.tagId
  181. where
  182. win.appId = @v2
  183. and (
  184. timeStart between @v0 and @v1
  185. or timeEnd between @v0 and @v1
  186. or @v0 between timeStart and timeEnd
  187. )
  188. group by win.id
  189. order by days desc
  190. ",
  191. timefrom, timeto, parentKey
  192. );
  193. foreach (var i in data)
  194. {
  195. var time = new TimeSpan((long)(i.days * TimeSpan.TicksPerDay));
  196. result.Add(
  197. new
  198. {
  199. i.winId,
  200. i.tagText,
  201. text = string.IsNullOrWhiteSpace(i.winText) ? "(无标题)" : i.winText,
  202. time = time.ToString(@"hh\:mm\:ss"),
  203. percent = Math.Round(time.TotalSeconds * 100 / totalSeconds, 2) + "%"
  204. }
  205. );
  206. }
  207. return result;
  208. }
  209. if (parentKey == 0)
  210. {
  211. data = db.ExecuteDynamic(@"
  212. select
  213. ifnull(tag.id,-1) tagId,
  214. ifnull(tag.text, '()') tagText,
  215. sum(julianday(case when timeEnd > @v1 then @v1 else timeEnd end) -
  216. julianday(case when timeStart < @v0 then @v0 else timeStart end)) days
  217. from win
  218. join app on app.id = win.appid
  219. join period p on p.winid = win.id
  220. left join tag on tag.id = win.tagId or (win.tagId = 0 and tag.id = app.tagId)
  221. where
  222. timeStart between @v0 and @v1
  223. or timeEnd between @v0 and @v1
  224. or @v0 between timeStart and timeEnd
  225. group by tag.id
  226. order by days desc
  227. ",
  228. timefrom, timeto
  229. );
  230. foreach (var i in data)
  231. {
  232. var time = new TimeSpan((long)(i.days * TimeSpan.TicksPerDay));
  233. result.Add(
  234. new
  235. {
  236. i.tagId,
  237. i.tagText,
  238. time = time.ToString(@"hh\:mm\:ss"),
  239. percent = Math.Round(time.TotalSeconds * 100 / totalSeconds, 2) + "%"
  240. }
  241. );
  242. }
  243. return result;
  244. }
  245. data = db.ExecuteDynamic(@"
  246. select
  247. win.id winId,
  248. win.text winText,
  249. sum(julianday(case when timeEnd > @v1 then @v1 else timeEnd end) -
  250. julianday(case when timeStart < @v0 then @v0 else timeStart end)) days
  251. from win
  252. join period p on p.winid = win.id
  253. join app on app.id = win.appid
  254. left join tag on tag.id = win.tagId or (win.tagId = 0 and tag.id = app.tagId)
  255. where
  256. ifnull(tag.id, -1) = @v2
  257. and (
  258. timeStart between @v0 and @v1
  259. or timeEnd between @v0 and @v1
  260. or @v0 between timeStart and timeEnd
  261. )
  262. group by win.id
  263. order by days desc
  264. ",
  265. timefrom, timeto, parentKey
  266. );
  267. foreach (var i in data)
  268. {
  269. var time = new TimeSpan((long)(i.days * TimeSpan.TicksPerDay));
  270. result.Add(
  271. new
  272. {
  273. i.winId,
  274. text = string.IsNullOrWhiteSpace(i.winText) ? "(无标题)" : i.winText,
  275. time = time.ToString(@"hh\:mm\:ss"),
  276. percent = Math.Round(time.TotalSeconds * 100 / totalSeconds, 2) + "%"
  277. }
  278. );
  279. }
  280. return result;
  281. }
  282. /// <summary>
  283. /// 时间
  284. /// </summary>
  285. public class TimeInfo
  286. {
  287. /// <summary>
  288. /// 原始时间
  289. /// </summary>
  290. public DateTime timeSrc;
  291. /// <summary>
  292. /// 切换到应用的时间
  293. /// </summary>
  294. public DateTime timeStart;
  295. /// <summary>
  296. /// 应用名
  297. /// </summary>
  298. public string app;
  299. /// <summary>
  300. /// 应用id
  301. /// </summary>
  302. public long appId;
  303. /// <summary>
  304. /// 窗口标题
  305. /// </summary>
  306. public string title;
  307. }
  308. /// <summary>
  309. /// 获取指定时间的记录信息
  310. /// </summary>
  311. /// <param name="time"></param>
  312. /// <returns></returns>
  313. public TimeInfo getTimeInfo(DateTime time)
  314. {
  315. var data = db.ExecuteDynamic(@"
  316. SELECT timeStart, app.text appText, win.text winText, app.id appId
  317. from period
  318. join win on win.id = period.winid
  319. join app on app.id = win.appid
  320. where @v0 between timeStart and timeEnd
  321. limit 1",
  322. time
  323. ).FirstOrDefault();
  324. if (data == null)
  325. {
  326. return null;
  327. }
  328. return new TimeInfo
  329. {
  330. timeSrc = time,
  331. timeStart = data.timeStart,
  332. app = data.appText,
  333. title = data.winText,
  334. appId = data.appId
  335. };
  336. }
  337. static byte[] defaultIcon;
  338. public byte[] getIcon(int appId, bool large)
  339. {
  340. var path = Recorder.GetIconPath(appId, large);
  341. if (File.Exists(path))
  342. {
  343. return File.ReadAllBytes(path);
  344. }
  345. if (defaultIcon == null)
  346. {
  347. defaultIcon = File.ReadAllBytes("./webui/img/icon.png");
  348. }
  349. return defaultIcon;
  350. }
  351. static byte[] imageNone = null;
  352. TimeSpan getTime(string file)
  353. {
  354. return TimeSpan.ParseExact(Path.GetFileNameWithoutExtension(file), "hhmmss", CultureInfo.InvariantCulture);
  355. }
  356. /// <summary>
  357. /// 查找不满足条件的最后一个元素
  358. /// </summary>
  359. /// <typeparam name="T"></typeparam>
  360. /// <param name="items"></param>
  361. /// <param name="largerThenTarget"></param>
  362. /// <returns></returns>
  363. T find<T>(IList<T> items, Func<T, bool> largerThenTarget)
  364. {
  365. if (items.Count == 0)
  366. {
  367. return default;
  368. }
  369. var match = items[0];
  370. if (largerThenTarget(match))
  371. {
  372. return default;
  373. }
  374. for (var i = 1; i < items.Count; i++)
  375. {
  376. var item = items[i];
  377. if (largerThenTarget(item))
  378. {
  379. break;
  380. }
  381. match = item;
  382. }
  383. return match;
  384. }
  385. static Thread lastThread;
  386. static readonly object threadLock = new object();
  387. /// <summary>
  388. /// 获取指定时间的截图
  389. /// </summary>
  390. /// <param name="info"></param>
  391. /// <returns></returns>
  392. public byte[] getImage(TimeInfo info)
  393. {
  394. if (imageNone == null)
  395. {
  396. imageNone = File.ReadAllBytes(Path.Combine(Server.WebRootPath, "img", "none.png"));
  397. }
  398. //只响应最后一个请求,避免运行多个ffmpeg占用资源。
  399. lock (threadLock)
  400. {
  401. Ffmpeg.KillLastFfmpeg();
  402. if (lastThread != null && lastThread.IsAlive)
  403. {
  404. lastThread.Abort();
  405. }
  406. lastThread = Thread.CurrentThread;
  407. }
  408. try
  409. {
  410. //先从buffer中找
  411. {
  412. var buffers = new List<MemoryBuffer>(Recorder.flushing);
  413. if (Recorder.buffer != null)
  414. {
  415. buffers.Add(Recorder.buffer);
  416. }
  417. var match = find(buffers, i => i.StartTime > info.timeSrc);
  418. if (match != null)
  419. {
  420. var time = info.timeSrc - match.StartTime;
  421. var frame = find(match.Frames, f => (match.StartTime + f.Time) > info.timeSrc);
  422. if (frame != null)
  423. {
  424. return frame.Data;
  425. }
  426. }
  427. }
  428. //从文件系统找
  429. {
  430. var path = Recorder.getFileName(info.timeSrc);
  431. var needtime = info.timeSrc.TimeOfDay;
  432. var needtimetext = needtime.ToString("hhmmss");
  433. var match = (from f in Directory.GetFiles(Path.GetDirectoryName(path), "????????." + Recorder.ExName)
  434. where Path.GetFileNameWithoutExtension(f).CompareTo(needtimetext) < 0
  435. orderby f
  436. select f).LastOrDefault();
  437. if (match != null)
  438. {
  439. var time = needtime - getTime(match);
  440. var data = Ffmpeg.Snapshot(match, time);
  441. if (data != null && data.Length > 0)
  442. {
  443. return data;
  444. }
  445. }
  446. return imageNone;
  447. }
  448. }
  449. catch (ThreadAbortException)
  450. {
  451. return imageNone;
  452. }
  453. }
  454. #endregion
  455. #region tag
  456. private long nextTagId = 0;
  457. private long NextTagId()
  458. {
  459. if (nextTagId == 0)
  460. {
  461. nextTagId = db.ExecuteValue<long>("select ifnull(max(id), 0) + 1 from tag");
  462. }
  463. return nextTagId++;
  464. }
  465. public bool existsTag(string text)
  466. {
  467. return db.ExecuteValue< bool>(
  468. "select exists(select * from tag where text = @text)",
  469. new SQLiteParameter("text", text)
  470. );
  471. }
  472. public bool addTag(string text)
  473. {
  474. if(existsTag(text))
  475. {
  476. return false;
  477. }
  478. db.Execute(
  479. "insert into tag (id, text) values(@id, @text)",
  480. new SQLiteParameter("id", NextTagId()),
  481. new SQLiteParameter("text", text)
  482. );
  483. return true;
  484. }
  485. public void removeTag(int tagId)
  486. {
  487. db.Execute(
  488. "delete from tag where id = @id",
  489. new SQLiteParameter("id", tagId)
  490. );
  491. db.Execute($"update app set tagId=0 where tagId={tagId}");
  492. db.Execute($"update win set tagId=0 where tagId={tagId}");
  493. }
  494. public void clearAppTag(long appId)
  495. {
  496. db.Execute("update app set tagid = 0 where id = @v0", appId);
  497. }
  498. public void clearWinTag(long winId)
  499. {
  500. db.Execute("update win set tagid = 0 where id = @v0", winId);
  501. }
  502. public DataTable getTags()
  503. {
  504. return db.ExecuteTable("select id, text from tag order by id");
  505. }
  506. public bool isTagUsed(int tagId)
  507. {
  508. return db.ExecuteValue<bool>(
  509. @"select exists(
  510. select * from app where tagId = @tagId
  511. union all
  512. select * from win where tagId = @tagId
  513. )",
  514. new SQLiteParameter("tagId", tagId)
  515. );
  516. }
  517. DB db = DB.Instance;
  518. public bool renameTag(long tagId, string newName)
  519. {
  520. if(existsTag(newName))
  521. {
  522. return false;
  523. }
  524. db.Execute("update tag set text=@newName where id=@tagId",
  525. new SQLiteParameter("newName", newName),
  526. new SQLiteParameter("tagId", tagId)
  527. );
  528. return true;
  529. }
  530. public void tagApp(long appId, long tagId)
  531. {
  532. db.Execute(
  533. "update app set tagid = @tagId where id=@appId",
  534. new SQLiteParameter("appId", appId),
  535. new SQLiteParameter("tagId", tagId)
  536. );
  537. }
  538. public void tagWin(long winId, long tagId)
  539. {
  540. db.Execute(
  541. "update win set tagid = @tagId where id=@winId",
  542. new SQLiteParameter("winId", winId),
  543. new SQLiteParameter("tagId", tagId)
  544. );
  545. }
  546. #endregion
  547. }
  548. }