PageRenderTime 61ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/Application/Core/FilesystemManager.cs

http://yet-another-music-application.googlecode.com/
C# | 1932 lines | 1123 code | 244 blank | 565 comment | 299 complexity | 3d6145905b877a842b719db9350f4666 MD5 | raw file
  1. /**
  2. * FilesystemManager.cs
  3. *
  4. * Handles all interaction with the filesystem such as scanning,
  5. * writing and reading the disk as well as watching for changes.
  6. *
  7. * * * * * * * * *
  8. *
  9. * Copyright 2012 Simplare
  10. *
  11. * This code is part of the Stoffi Music Player Project.
  12. * Visit our website at: stoffiplayer.com
  13. *
  14. * This program is free software; you can redistribute it and/or
  15. * modify it under the terms of the GNU General Public License
  16. * as published by the Free Software Foundation; either version
  17. * 3 of the License, or (at your option) any later version.
  18. *
  19. * See stoffiplayer.com/license for more information.
  20. **/
  21. using Microsoft.WindowsAPICodePack.Shell;
  22. using System;
  23. using System.Collections;
  24. using System.Collections.Generic;
  25. using System.Collections.ObjectModel;
  26. using System.ComponentModel;
  27. using System.IO;
  28. using System.Linq;
  29. using System.Security.AccessControl;
  30. using System.Text;
  31. using System.Threading;
  32. using Un4seen.Bass;
  33. using Un4seen.Bass.AddOn.Tags;
  34. namespace Stoffi
  35. {
  36. /// <summary>
  37. /// Represents a manager that takes care of talking to the filesystem
  38. /// </summary>
  39. public static class FilesystemManager
  40. {
  41. #region Members
  42. private static List<FileSystemWatcher> janitors = new List<FileSystemWatcher>();
  43. private static Timer janitorLazyness = null;
  44. private static List<JanitorTask> janitorTasks = new List<JanitorTask>();
  45. private static Thread scanThread = null;
  46. private static string librariesPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + @"\AppData\Roaming\Microsoft\Windows\Libraries\";
  47. private static List<SourceTree> sourceForest = new List<SourceTree>();
  48. #endregion
  49. #region Properties
  50. /// <summary>
  51. /// Gets or sets the indicator that the scanner will listen to in order to know if it
  52. /// should die gracefully. To be used when the application wants to close.
  53. /// </summary>
  54. public static bool ProgramIsClosed { get; set; }
  55. /// <summary>
  56. /// Gets whether the manager has been initialized
  57. /// </summary>
  58. public static bool IsInitialized { get; private set; }
  59. /// <summary>
  60. /// Gets the current status of the scanner
  61. /// </summary>
  62. public static bool IsScanning
  63. {
  64. get { return scanThread.IsAlive; }
  65. }
  66. #endregion
  67. #region Constructor
  68. /// <summary>
  69. /// Creates a filesystem manager
  70. /// </summary>
  71. static FilesystemManager()
  72. {
  73. IsInitialized = false;
  74. }
  75. #endregion
  76. #region Methods
  77. #region Public
  78. /// <summary>
  79. /// Initializes the filesystem manager by setting up library watchers and event handlers
  80. /// </summary>
  81. public static void Initialize()
  82. {
  83. ProgramIsClosed = false;
  84. // setup a janitor for libraries
  85. FileSystemWatcher fsw = new FileSystemWatcher();
  86. fsw.Path = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + @"\AppData\Roaming\Microsoft\Windows\Libraries\";
  87. fsw.Filter = "*.library-ms";
  88. fsw.IncludeSubdirectories = false;
  89. fsw.NotifyFilter = NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size;
  90. fsw.Changed += Janitor_LibraryChanged;
  91. fsw.Created += Janitor_LibraryChanged;
  92. fsw.Deleted += Janitor_LibraryChanged;
  93. fsw.Renamed += Janitor_LibraryRenamed;
  94. fsw.EnableRaisingEvents = true;
  95. janitors.Add(fsw);
  96. // setup event handler to check if track changes
  97. foreach (TrackData t in SettingsManager.FileTracks)
  98. if (t != null)
  99. t.PropertyChanged += new PropertyChangedEventHandler(Track_PropertyChanged);
  100. foreach (TrackData t in SettingsManager.HistoryTracks)
  101. if (t != null)
  102. t.PropertyChanged += new PropertyChangedEventHandler(Track_PropertyChanged);
  103. foreach (TrackData t in SettingsManager.QueueTracks)
  104. if (t != null)
  105. t.PropertyChanged += new PropertyChangedEventHandler(Track_PropertyChanged);
  106. foreach (PlaylistData p in SettingsManager.Playlists)
  107. foreach (TrackData t in p.Tracks)
  108. if (t != null)
  109. t.PropertyChanged += new PropertyChangedEventHandler(Track_PropertyChanged);
  110. IsInitialized = true;
  111. }
  112. /// <summary>
  113. /// Get a specific track
  114. /// </summary>
  115. /// <param name="filename">The filename of the track</param>
  116. /// <returns>The TrackData for the track</returns>
  117. public static TrackData GetTrack(String filename)
  118. {
  119. foreach (TrackData t in SettingsManager.FileTracks)
  120. if (t.Path == filename)
  121. return t;
  122. return null;
  123. }
  124. /// <summary>
  125. /// Writes the current track data to the file
  126. /// </summary>
  127. /// <param name="track">The track to save</param>
  128. public static void SaveTrack(TrackData track)
  129. {
  130. TagLib.File file = TagLib.File.Create(track.Path);
  131. file.Tag.Performers = track.Artist.Split(',');
  132. file.Tag.Album = track.Album;
  133. file.Tag.Title = track.Title;
  134. file.Tag.Year = Convert.ToUInt32(track.Year);
  135. file.Tag.Genres = track.Genre.Split(',');
  136. file.Tag.Track = Convert.ToUInt32(track.Track);
  137. file.Save();
  138. UpdateTrack(track);
  139. }
  140. /// <summary>
  141. /// Checks if the meta data of a track has been updated since last read.
  142. /// </summary>
  143. /// <param name="track">The track to check</param>
  144. /// <returns>True if the track has been written to since last read</returns>
  145. public static bool Updated(TrackData track)
  146. {
  147. try
  148. {
  149. FileInfo fInfo = new FileInfo(track.Path);
  150. return fInfo.LastWriteTimeUtc.Ticks > track.LastWrite;
  151. }
  152. catch
  153. {
  154. return false;
  155. }
  156. }
  157. /// <summary>
  158. /// Reads the metadata of a track and updates it
  159. /// </summary>
  160. /// <param name="track">The track to update</param>
  161. /// <param name="copy">Whether the data should be copied to queue/history/playlists</param>
  162. public static void UpdateTrack(TrackData track, bool copy = true)
  163. {
  164. if (!Updated(track)) return;
  165. try
  166. {
  167. FileInfo fInfo = new FileInfo(track.Path);
  168. TagLib.File file = TagLib.File.Create(track.Path, TagLib.ReadStyle.Average);
  169. track.Artist = U.CleanXMLString(file.Tag.JoinedPerformers);
  170. track.Album = U.CleanXMLString(file.Tag.Album);
  171. track.Title = U.CleanXMLString(file.Tag.Title);
  172. track.Genre = U.CleanXMLString(file.Tag.JoinedGenres);
  173. track.Track = file.Tag.Track;
  174. track.Year = file.Tag.Year;
  175. track.Length = U.TimeSpanToString(file.Properties.Duration);
  176. track.RawLength = file.Properties.Duration.TotalSeconds;
  177. track.LastWrite = fInfo.LastWriteTimeUtc.Ticks;
  178. track.Bitrate = file.Properties.AudioBitrate;
  179. track.Channels = file.Properties.AudioChannels;
  180. track.SampleRate = file.Properties.AudioSampleRate;
  181. track.Codecs = "";
  182. foreach (TagLib.ICodec c in file.Properties.Codecs)
  183. track.Codecs += c.Description + ", ";
  184. track.Codecs = track.Codecs.Substring(0, track.Codecs.Length - 2);
  185. track.Processed = true;
  186. }
  187. catch (Exception e)
  188. {
  189. if (track.Artist == U.T("MetaDataLoading")) track.Artist = "";
  190. if (track.Album == U.T("MetaDataLoading")) track.Album = "";
  191. if (track.Title == U.T("MetaDataLoading")) track.Title = "";
  192. if (track.Genre == U.T("MetaDataLoading")) track.Genre = "";
  193. if (track.Length == U.T("MetaDataLoading")) track.Length = "";
  194. U.L(LogLevel.Warning, "FILESYSTEM", "Could not read ID3 data of file: " + track.Path);
  195. U.L(LogLevel.Warning, "FILESYSTEM", e.Message);
  196. }
  197. track.Icon = "pack://application:,,,/Platform/Windows/GUI/Images/Icons/FileAudio.ico";
  198. if (!copy) return;
  199. for (int i = 0; i < SettingsManager.FileTracks.Count; i++)
  200. {
  201. if (SettingsManager.FileTracks[i].Path == track.Path)
  202. {
  203. CopyTrackInfo(track, SettingsManager.FileTracks[i]);
  204. break;
  205. }
  206. }
  207. for (int i = 0; i < SettingsManager.QueueTracks.Count; i++)
  208. {
  209. if (SettingsManager.QueueTracks[i].Path == track.Path)
  210. {
  211. CopyTrackInfo(track, SettingsManager.QueueTracks[i]);
  212. break;
  213. }
  214. }
  215. for (int i = 0; i < SettingsManager.HistoryTracks.Count; i++)
  216. {
  217. if (SettingsManager.HistoryTracks[i].Path == track.Path)
  218. {
  219. CopyTrackInfo(track, SettingsManager.HistoryTracks[i]);
  220. break;
  221. }
  222. }
  223. foreach (PlaylistData p in SettingsManager.Playlists)
  224. for (int i = 0; i < p.Tracks.Count; i++)
  225. {
  226. if (p.Tracks[i].Path == track.Path)
  227. {
  228. CopyTrackInfo(track, p.Tracks[i]);
  229. break;
  230. }
  231. }
  232. }
  233. /// <summary>
  234. /// Toggle a source between Include and Ignore
  235. /// </summary>
  236. /// <param name="source">The source to toggle</param>
  237. public static void ToggleSource(SourceData source)
  238. {
  239. source.Include = !source.Include;
  240. ScanSources();
  241. }
  242. /// <summary>
  243. /// Add a source to the collection
  244. /// </summary>
  245. /// <param name="path">The path of source to be added</param>
  246. /// <param name="callback">A function that will be sent along with the SourceModified event</param>
  247. /// <param name="callbackParams">The parameters for the callback function</param>
  248. public static void AddSource(string path, ScannerCallback callback = null, object callbackParams = null)
  249. {
  250. if (File.Exists(path))
  251. {
  252. AddSource(new SourceData()
  253. {
  254. Data = path,
  255. Type = SourceType.File,
  256. HumanType = U.T("SourcesTypeFile"),
  257. Icon = "pack://application:,,,/Platform/Windows/GUI/Images/Icons/FileAudio.ico",
  258. Include = true
  259. }, callback, callbackParams);
  260. }
  261. else if (Directory.Exists(path))
  262. {
  263. AddSource(new SourceData()
  264. {
  265. Data = path,
  266. Type = SourceType.Folder,
  267. HumanType = U.T("SourcesTypeFolder"),
  268. Icon = "pack://application:,,,/Platform/Windows/GUI/Images/Icons/Folder.ico",
  269. Include = true
  270. }, callback, callbackParams);
  271. }
  272. else
  273. {
  274. AddSource(new SourceData()
  275. {
  276. Data = path,
  277. Type = SourceType.Library,
  278. HumanType = U.T("SourcesTypeLibrary"),
  279. Icon = "pack://application:,,,/Platform/Windows/GUI/Images/Icons/Library.ico",
  280. Include = true
  281. }, callback, callbackParams);
  282. }
  283. }
  284. /// <summary>
  285. /// Add a source to the collection
  286. /// </summary>
  287. /// <param name="source">The source to be added</param>
  288. /// <param name="callback">A function that will be sent along with the SourceModified event</param>
  289. /// <param name="callbackParams">The parameters for the callback function</param>
  290. public static void AddSource(SourceData source, ScannerCallback callback = null, object callbackParams = null)
  291. {
  292. U.L(LogLevel.Information, "FILESYSTEM", String.Format("{0} the {1} {2}",
  293. (source.Include ? "Adding" : "Ignoring"), source.Type, source.Data));
  294. SourceTree s = GetSourceTree(source.Data, sourceForest);
  295. if (s != null)
  296. {
  297. U.L(LogLevel.Warning, "FILESYSTEM", String.Format("Removing currently {0} {1} {2}",
  298. (source.Include ? "added" : "ignored"), source.Type, source.Data));
  299. RemoveSource(source);
  300. }
  301. DispatchSourceAdded(source);
  302. ScanSources(callback, callbackParams);
  303. }
  304. /// <summary>
  305. /// Remove a source from the collection
  306. /// </summary>
  307. /// <param name="source">The source to be removed</param>
  308. public static void RemoveSource(SourceData source)
  309. {
  310. if (!SettingsManager.Sources.Contains(source))
  311. {
  312. U.L(LogLevel.Warning, "FILESYSTEM", "Trying to remove non-existing source " + source.Data);
  313. return;
  314. }
  315. DispatchSourceRemoved(source);
  316. ScanSources();
  317. }
  318. /// <summary>
  319. /// Retrieves a source given its data
  320. /// </summary>
  321. /// <param name="data">The data of the source</param>
  322. /// <returns>A source matching criterion if found, otherwise null</returns>
  323. public static SourceData GetSource(string data)
  324. {
  325. foreach (SourceData s in SettingsManager.Sources)
  326. if (s.Data == data)
  327. return s;
  328. return null;
  329. }
  330. /// <summary>
  331. /// Scans for Windows 7 Libraries of type Music and adds them to the collection.
  332. /// </summary>
  333. /// <param name="ScanSourcesWhenDone">Set to true to scan the folders afterwards</param>
  334. /// <param name="ScanInBackground">Set to true to perform the scan in a background thread</param>
  335. public static void ScanLibraries(bool ScanSourcesWhenDone = false, bool ScanInBackground = true)
  336. {
  337. WaitForInitialization();
  338. if (ScanInBackground)
  339. {
  340. ThreadStart ScanThread = delegate()
  341. {
  342. ScanLibrariesFunc(ScanSourcesWhenDone);
  343. };
  344. Thread thread = new Thread(ScanThread);
  345. thread.Name = "Scan libraries";
  346. thread.Priority = ThreadPriority.BelowNormal;
  347. thread.Start();
  348. }
  349. else
  350. ScanLibrariesFunc(ScanSourcesWhenDone);
  351. }
  352. /// <summary>
  353. /// Scan all folders in the collection for supported music tracks
  354. /// </summary>
  355. /// <param name="callback">A function that will be sent along with the SourceModified event</param>
  356. /// <param name="callbackParams">The parameters for the callback function</param>
  357. public static void ScanSources(ScannerCallback callback = null, object callbackParams = null)
  358. {
  359. WaitForInitialization();
  360. StopScan();
  361. ThreadStart ScanThread = delegate()
  362. {
  363. U.L(LogLevel.Information, "FILESYSTEM", "Starting scan of sources");
  364. DispatchProgressChanged(0, "start");
  365. U.L(LogLevel.Debug, "FILESYSTEM", "Refreshing meta data of existing tracks");
  366. foreach (TrackData t in SettingsManager.FileTracks)
  367. {
  368. if (Updated(t))
  369. DispatchSourceModified(t, SourceModificationType.Updated);
  370. }
  371. U.L(LogLevel.Debug, "FILESYSTEM", "Getting forest");
  372. List<SourceTree> forest = GetSourceForest();
  373. List<SourceTree> addedPaths = new List<SourceTree>();
  374. List<SourceTree> removedPaths = new List<SourceTree>();
  375. U.L(LogLevel.Debug, "FILESYSTEM", "Calculating forest change");
  376. GetForestDifference(sourceForest, forest, addedPaths, removedPaths);
  377. //U.L(LogLevel.Debug, "FILESYSTEM", String.Format("Removing {0} paths ", removedPaths.Count));
  378. //foreach (SourceTree tree in removedPaths)
  379. //{
  380. // bool added = PathIsAdded(tree.Data, forest);
  381. // bool ignored = PathIsIgnored(tree.Data, forest);
  382. // if ((tree.Include && added) || (tree.Ignore && (!added || ignored)))
  383. // continue;
  384. // // see if there's any other sources on the same drive
  385. // bool remove = true;
  386. // foreach (SourceTree t in forest)
  387. // {
  388. // if (Path.GetPathRoot(t.Data) == Path.GetPathRoot(tree.Data))
  389. // {
  390. // remove = false;
  391. // break;
  392. // }
  393. // }
  394. // if (remove)
  395. // RemoveJanitor(Path.GetPathRoot(tree.Data));
  396. // tree.Ignore = tree.Include;
  397. // ScanPath(tree.Data, tree, true, callback, callbackParams);
  398. //}
  399. U.L(LogLevel.Debug, "FILESYSTEM", String.Format("Adding {0} paths", addedPaths.Count));
  400. foreach (SourceTree tree in addedPaths)
  401. {
  402. SetupJanitor(Path.GetPathRoot(tree.Data));
  403. ScanPath(tree.Data, tree, true, callback, callbackParams);
  404. }
  405. U.L(LogLevel.Debug, "FILESYSTEM", String.Format("Checking existing tracks"));
  406. foreach (TrackData track in SettingsManager.FileTracks)
  407. {
  408. if (PathIsIgnored(track.Path) || !File.Exists(track.Path))
  409. {
  410. RemoveFile(track.Path, callback, callbackParams);
  411. }
  412. }
  413. if (addedPaths.Count == 0 && removedPaths.Count == 0 && callback != null)
  414. callback(callbackParams);
  415. sourceForest = forest;
  416. List<TrackData> tracksToRemove = new List<TrackData>();
  417. foreach (TrackData t in SettingsManager.FileTracks)
  418. {
  419. // remove files that should not be in collection
  420. if (!PathIsAdded(t.Path))
  421. tracksToRemove.Add(t);
  422. }
  423. if (tracksToRemove.Count > 0)
  424. U.L(LogLevel.Debug, "FILESYSTEM", "Dispatching SourceModified for all removals");
  425. foreach (TrackData t in tracksToRemove)
  426. DispatchSourceModified(t, SourceModificationType.Removed);
  427. DispatchProgressChanged(100, "done");
  428. U.L(LogLevel.Information, "FILESYSTEM", "Finished scan of sources");
  429. };
  430. scanThread = new Thread(ScanThread);
  431. scanThread.Name = "Scan thread";
  432. scanThread.Priority = ThreadPriority.Lowest;
  433. // read the meta data
  434. ThreadStart updateThread = delegate()
  435. {
  436. // TODO: speed this up!
  437. // wait for scan thread
  438. U.L(LogLevel.Debug, "FILESYSTEM", "Meta scanner is waiting for file scanner");
  439. while (SettingsManager.FileTracks.Count == 0 && scanThread.IsAlive) ;
  440. for (int i = 0; i < SettingsManager.FileTracks.Count; i++)
  441. {
  442. // break if program is closing
  443. if (ProgramIsClosed) break;
  444. // wait for scan thread
  445. while (i >= SettingsManager.FileTracks.Count - 1 && scanThread.IsAlive);
  446. // update track
  447. if (i < SettingsManager.FileTracks.Count)
  448. FilesystemManager.UpdateTrack(SettingsManager.FileTracks[i]);
  449. }
  450. };
  451. Thread metathread = new Thread(updateThread);
  452. metathread.Name = "Read Meta thread";
  453. metathread.Priority = ThreadPriority.Lowest;
  454. if (scanThread.IsAlive) scanThread.Abort();
  455. scanThread.Start();
  456. //metathread.Start();
  457. }
  458. /// <summary>
  459. /// Stop the scanner
  460. /// </summary>
  461. public static void StopScan()
  462. {
  463. if (scanThread != null && scanThread.IsAlive)
  464. scanThread.Abort();
  465. }
  466. /// <summary>
  467. /// Create a track given a filename
  468. /// </summary>
  469. /// <param name="filename">The filename of the track</param>
  470. /// <param name="dispatchModified">Whether or not to dispatch that the track has been added</param>
  471. /// <returns>The newly created track</returns>
  472. public static TrackData CreateTrack(String filename, bool dispatchModified = true)
  473. {
  474. if (!MediaManager.IsSupported(filename))
  475. {
  476. U.L(LogLevel.Warning, "FILESYSTEM", "Cannot create track " + filename + ": unsupported format");
  477. return null;
  478. }
  479. if (!File.Exists(filename))
  480. {
  481. U.L(LogLevel.Warning, "FILESYSTEM", "Cannot create track " + filename + ": file does not exist");
  482. return null;
  483. }
  484. FileInfo fInfo = new FileInfo(filename);
  485. TrackData track = new TrackData
  486. {
  487. Processed = false,
  488. Artist = U.T("MetaDataLoading"),
  489. Album = U.T("MetaDataLoading"),
  490. Title = U.T("MetaDataLoading"),
  491. Genre = U.T("MetaDataLoading"),
  492. Year = 0,
  493. Length = U.T("MetaDataLoading"),
  494. RawLength = 0,
  495. PlayCount = 0,
  496. LastPlayed = "",
  497. Track = 0,
  498. Path = filename,
  499. Bitrate = 0,
  500. Channels = 0,
  501. SampleRate = 0,
  502. Codecs = "",
  503. Number = 0,
  504. Bookmarks = new List<double>(),
  505. Source = "Library",
  506. Icon = @"..\..\Platform\Windows\GUI\Images\Icons\FileAudio.ico",
  507. LastWrite = 0
  508. };
  509. track.PropertyChanged += new PropertyChangedEventHandler(Track_PropertyChanged);
  510. if (dispatchModified)
  511. DispatchSourceModified(track, SourceModificationType.Added);
  512. return track;
  513. }
  514. /// <summary>
  515. /// Check of a given path is being watched
  516. /// </summary>
  517. /// <param name="path">The path to check</param>
  518. /// <returns>true if <paramref name="path"/> is being watched, otherwise false</returns>
  519. public static bool PathIsAdded(String path)
  520. {
  521. return PathIsAdded(path, sourceForest);
  522. }
  523. /// <summary>
  524. /// Check of a given path is being watched
  525. /// </summary>
  526. /// <param name="path">The path to check</param>
  527. /// <param name="forest">The forest to check in</param>
  528. /// <returns>true if <paramref name="path"/> is being watched, otherwise false</returns>
  529. public static bool PathIsAdded(String path, List<SourceTree> forest)
  530. {
  531. SourceTree s = GetSourceTree(path, forest, false);
  532. if (s == null)
  533. return false;
  534. else
  535. return s.Include;
  536. }
  537. /// <summary>
  538. /// Check of a given path is being ignored
  539. /// </summary>
  540. /// <param name="path">The path to check</param>
  541. /// <returns>true if <paramref name="path"/> is being ignored, otherwise false</returns>
  542. public static bool PathIsIgnored(String path)
  543. {
  544. return PathIsIgnored(path, sourceForest);
  545. }
  546. /// <summary>
  547. /// Check of a given path is being ignored
  548. /// </summary>
  549. /// <param name="path">The path to check</param>
  550. /// <param name="forest">The forest to check in</param>
  551. /// <returns>true if <paramref name="path"/> is being ignored, otherwise false</returns>
  552. public static bool PathIsIgnored(String path, List<SourceTree> forest)
  553. {
  554. SourceTree s = GetSourceTree(path, forest, false);
  555. if (s == null) return false;
  556. else return s.Ignore;
  557. }
  558. /// <summary>
  559. /// Checks if a track has been added to a collection of tracks.
  560. /// </summary>
  561. /// <param name="path">The path to check for</param>
  562. /// <param name="tracks">The collection to check in</param>
  563. /// <returns>True if a track with the same path is found</returns>
  564. public static bool TrackIsAdded(string path, ObservableCollection<TrackData> tracks)
  565. {
  566. foreach (TrackData t in tracks)
  567. if (t.Path == path)
  568. return true;
  569. return false;
  570. }
  571. /// <summary>
  572. /// Checks if a track has been added to the file collection.
  573. /// </summary>
  574. /// <param name="path">The path to check for</param>
  575. /// <returns>True if a track with the same path is found</returns>
  576. public static bool TrackIsAdded(string path)
  577. {
  578. return TrackIsAdded(path, SettingsManager.FileTracks);
  579. }
  580. #endregion
  581. #region Private
  582. /// <summary>
  583. /// Pauses until both the filesystem manager and media manager has been initialized
  584. /// </summary>
  585. private static void WaitForInitialization()
  586. {
  587. if (!MediaManager.IsInitialized)
  588. {
  589. U.L(LogLevel.Debug, "FILESYSTEM", "Waiting for Media Manager to be initialized, pausing scanner");
  590. while (!MediaManager.IsInitialized) ;
  591. U.L(LogLevel.Debug, "FILESYSTEM", "Media Manager has been detected as initialized, resuming scanner");
  592. }
  593. if (!IsInitialized)
  594. {
  595. U.L(LogLevel.Debug, "FILESYSTEM", "Waiting for Filesystem Manager to be initialized, pausing scanner");
  596. while (!IsInitialized) ;
  597. U.L(LogLevel.Debug, "FILESYSTEM", "Filesystem Manager has been detected as initialized, resuming scanner");
  598. }
  599. }
  600. /// <summary>
  601. /// Scan a path for files and synchronize its content with our collection
  602. /// </summary>
  603. /// <param name="path">The path to scan</param>
  604. /// <param name="tree">The tree that is either the path itself or the parent</param>
  605. /// <param name="progressPosition">The initial progress position when the scan starts</param>
  606. /// <param name="deltaProgress">The amount of progress (in percentage points) that this scan will amount for</param>
  607. /// <param name="removeIgnored">Whether or not we should remove any paths that we find which is set to ignore</param>
  608. /// <param name="callback">A function that will be sent along with the SourceModified event</param>
  609. /// <param name="callbackParams">The parameters for the callback function</param>
  610. private static void ScanPath(String path, SourceTree tree, bool removeIgnored = true, ScannerCallback callback = null, object callbackParams = null)
  611. {
  612. // ignore the recycle bin
  613. if (path.Substring(1).ToUpper().StartsWith(@":\$RECYCLE.BIN")) return;
  614. // break if program is closing
  615. if (ProgramIsClosed) return;
  616. WaitForInitialization();
  617. // path is a file
  618. if (File.Exists(path))
  619. {
  620. if (tree.Include) AddFile(path, false, callback, callbackParams);
  621. else if (removeIgnored) RemoveFile(path, callback, callbackParams);
  622. }
  623. // path is a folder
  624. else if (Directory.Exists(path))
  625. {
  626. string[] files = Directory.GetFiles(path);
  627. string[] folders = Directory.GetDirectories(path);
  628. // calculate the amount of progress to increase each file/folder with
  629. //double progressIncrease = deltaProgress / (files.Count() + folders.Count());
  630. // scan all toplevel files
  631. string[] ext = MediaManager.SupportedFormatsExtensions.Split(';');
  632. try
  633. {
  634. for (int i=0; i<files.Count(); i++)
  635. {
  636. // break if program is closing
  637. if (ProgramIsClosed) return;
  638. string file = files[i];
  639. //int progressPos = (int)(progressPosition + (i * progressIncrease));
  640. //DispatchProgressChanged(progressPos, "progress");
  641. if (!ext.Contains("*" + Path.GetExtension(file)))
  642. continue;
  643. bool include = tree.Include;
  644. SourceTree st = GetSourceTree(file, tree.Children);
  645. if (st != null)
  646. include = st.Include;
  647. foreach (SourceTree childTree in tree.Children)
  648. {
  649. if (childTree.Data == file)
  650. {
  651. include = childTree.Include;
  652. break;
  653. }
  654. }
  655. if (include) AddFile(file, false, callback, callbackParams);
  656. else if (removeIgnored) RemoveFile(file, callback, callbackParams);
  657. }
  658. // dive inside subfolders
  659. for (int i=0; i<folders.Count(); i++)
  660. {
  661. // break if program is closing
  662. if (ProgramIsClosed) return;
  663. string subfolder = folders[i];
  664. //int progressPos = (int)(progressPosition + ((files.Count() + i) * progressIncrease));
  665. //DispatchProgressChanged(progressPos, "progress");
  666. SourceTree subtree = GetSourceTree(subfolder, tree.Children);
  667. if (subtree == null) subtree = tree;
  668. ScanPath(subfolder, subtree, removeIgnored, callback, callbackParams);
  669. }
  670. }
  671. catch (Exception e)
  672. {
  673. // break if program is closing
  674. if (ProgramIsClosed) return;
  675. U.L(LogLevel.Warning, "FILESYSTEM", "Could not scan " + path + ": " + e.Message);
  676. }
  677. }
  678. }
  679. /// <summary>
  680. /// Copy all metadata from one track to another
  681. /// </summary>
  682. /// <param name="source">The source track from which the metadata is copied</param>
  683. /// <param name="destination">The destination track to which the metadata is copied</param>
  684. private static void CopyTrackInfo(TrackData source, TrackData destination)
  685. {
  686. destination.Artist = source.Artist;
  687. destination.Album = source.Album;
  688. destination.Genre = source.Genre;
  689. destination.Title = source.Title;
  690. destination.Track = source.Track;
  691. destination.Year = source.Year;
  692. destination.Length = source.Length;
  693. destination.RawLength = source.RawLength;
  694. destination.LastWrite = source.LastWrite;
  695. destination.Bitrate = source.Bitrate;
  696. destination.RawViews = source.RawViews;
  697. destination.Icon = source.Icon;
  698. destination.Codecs = source.Codecs;
  699. destination.Channels = source.Channels;
  700. destination.SampleRate = source.SampleRate;
  701. }
  702. /// <summary>
  703. /// Add a file to the collection
  704. /// </summary>
  705. /// <param name="filename">The path of the file</param>
  706. /// <param name="scanMetaData">Whether to scan the tracks meta data as well</param>
  707. /// <param name="callback">A function that will be sent along with the SourceModified event</param>
  708. /// <param name="callbackParams">The parameters for the callback function</param>
  709. private static void AddFile(String filename, bool scanMetaData = false, ScannerCallback callback = null, object callbackParams = null)
  710. {
  711. //ThreadStart addThread = delegate()
  712. //{
  713. //check if the file is already added
  714. //if (TrackIsAdded(filename))
  715. //{
  716. // if (callback != null)
  717. // callback(callbackParams);
  718. // return;
  719. //}
  720. FileInfo fInfo = new FileInfo(filename);
  721. TrackData track = new TrackData
  722. {
  723. Processed = false,
  724. Artist = U.T("MetaDataLoading"),
  725. Album = U.T("MetaDataLoading"),
  726. Title = U.T("MetaDataLoading"),
  727. Genre = U.T("MetaDataLoading"),
  728. Year = 0,
  729. Length = U.T("MetaDataLoading"),
  730. RawLength = 0,
  731. PlayCount = 0,
  732. LastPlayed = "",
  733. Bitrate = 0,
  734. Channels = 0,
  735. SampleRate = 0,
  736. Codecs = "",
  737. Track = 0,
  738. Path = fInfo.FullName,
  739. Number = 0,
  740. Icon = @"..\..\Platform\Windows\GUI\Images\Icons\FileAudio.ico",
  741. LastWrite = 0
  742. };
  743. DispatchSourceModified(track, SourceModificationType.Added, callback, callbackParams);
  744. track.PropertyChanged += new PropertyChangedEventHandler(Track_PropertyChanged);
  745. if (scanMetaData)
  746. DispatchPathModified(track.Path);
  747. //};
  748. //Thread addthread = new Thread(addThread);
  749. //addthread.Name = "Add file thread";
  750. //addthread.Priority = ThreadPriority.Lowest;
  751. //addthread.Start();
  752. }
  753. /// <summary>
  754. /// Remove a file from the collection
  755. /// </summary>
  756. /// <param name="filename">The path of the file</param>
  757. /// <param name="callback">A function that will be sent along with the SourceModified event</param>
  758. /// <param name="callbackParams">The parameters for the callback function</param>
  759. private static void RemoveFile(String filename, ScannerCallback callback = null, object callbackParams = null)
  760. {
  761. //ThreadStart delThread = delegate()
  762. //{
  763. TrackData track = null;
  764. foreach (TrackData t in SettingsManager.FileTracks)
  765. {
  766. if (t.Path == filename)
  767. {
  768. track = t;
  769. break;
  770. }
  771. }
  772. if (track != null)
  773. DispatchSourceModified(track, SourceModificationType.Removed, callback, callbackParams);
  774. else if (callback != null)
  775. callback(callbackParams);
  776. //};
  777. //Thread delthread = new Thread(delThread);
  778. //delthread.Name = "Remove file thread";
  779. //delthread.Priority = ThreadPriority.Lowest;
  780. //delthread.Start();
  781. }
  782. /// <summary>
  783. /// Changes the path of a set of tracks
  784. /// </summary>
  785. /// <param name="oldName">The current path of the tracks</param>
  786. /// <param name="newName">The new path of the tracks</param>
  787. /// <param name="tracks">The set of tracks</param>
  788. private static void RenamePath(String oldName, String newName, ObservableCollection<TrackData> tracks)
  789. {
  790. foreach (TrackData track in tracks)
  791. if (track.Path.StartsWith(oldName))
  792. track.Path = track.Path.Replace(oldName, newName);
  793. }
  794. /// <summary>
  795. /// Gets a specific source tree given its data
  796. /// </summary>
  797. /// <param name="data">The data of the source tree</param>
  798. /// <param name="forest">The parent of the source to look for</param>
  799. /// <param name="exactMatch">Whether the path match should be exact or just initial</param>
  800. /// <returns>The source with the corresponding data if one could be found, otherwise null</returns>
  801. private static SourceTree GetSourceTree(String data, List<SourceTree> forest, bool exactMatch = true)
  802. {
  803. foreach (SourceTree source in forest)
  804. {
  805. if (data == source.Data && exactMatch)
  806. return source;
  807. SourceTree child = GetSourceTree(data, source.Children, exactMatch);
  808. if (child != null) return child;
  809. else if (!exactMatch && data.StartsWith(source.Data))
  810. return source;
  811. }
  812. return null;
  813. }
  814. /// <summary>
  815. /// Gets a specific source given its data
  816. /// </summary>
  817. /// <param name="data">The data of the source</param>
  818. /// <returns>The source with the corresponding data if one could be found, otherwise null</returns>
  819. private static SourceData GetSourceData(String data)
  820. {
  821. foreach (SourceData s in SettingsManager.Sources)
  822. if (s.Data == data)
  823. return s;
  824. return null;
  825. }
  826. /// <summary>
  827. /// Gets a specific janitor
  828. /// </summary>
  829. /// <param name="drive">The drive that the janitor is responsible of</param>
  830. /// <returns>The janitor if one could be found, otherwise null</returns>
  831. private static FileSystemWatcher GetJanitor(String drive)
  832. {
  833. foreach (FileSystemWatcher fsw in janitors)
  834. if (fsw.Path == drive)
  835. return fsw;
  836. return null;
  837. }
  838. /// <summary>
  839. /// Creates a janitor for a specific drive
  840. /// </summary>
  841. /// <param name="drive">The drive that the janitor is responsible of</param>
  842. private static void SetupJanitor(String drive)
  843. {
  844. if (GetJanitor(drive) != null) return;
  845. FileSystemWatcher fsw = new FileSystemWatcher();
  846. if (!Directory.Exists(drive)) return;
  847. U.L(LogLevel.Debug, "FILESYSTEM", "Started watching for events on " + drive);
  848. fsw.Path = drive;
  849. fsw.IncludeSubdirectories = true;
  850. fsw.NotifyFilter = NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName |
  851. NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size;
  852. fsw.Changed += Janitor_FileChanged;
  853. fsw.Created += Janitor_FileChanged;
  854. fsw.Deleted += Janitor_FileChanged;
  855. fsw.Renamed += Janitor_FileRenamed;
  856. fsw.EnableRaisingEvents = true;
  857. janitors.Add(fsw);
  858. }
  859. /// <summary>
  860. /// Removes a specific janitor
  861. /// </summary>
  862. /// <param name="drive">The drive that the janitor is responsible of</param>
  863. private static void RemoveJanitor(String drive)
  864. {
  865. FileSystemWatcher fsw = GetJanitor(drive);
  866. if (fsw == null) return;
  867. U.L(LogLevel.Debug, "FILESYSTEM", "Stopped watching for events on " + drive);
  868. fsw.EnableRaisingEvents = false;
  869. janitors.Remove(fsw);
  870. }
  871. /// <summary>
  872. /// Get a list of folders inside a Windows 7 Library
  873. /// </summary>
  874. /// <param name="name">The name of the library</param>
  875. /// <returns>A list of all folders inside <paramref name="name"/></returns>
  876. private static List<String> GetLibraryFolders(string name)
  877. {
  878. List<string> ret = new List<string>();
  879. if (System.IO.File.Exists(librariesPath + name + ".library-ms"))
  880. {
  881. ShellLibrary lib = ShellLibrary.Load(name, librariesPath, true);
  882. foreach (ShellFileSystemFolder folder in lib)
  883. ret.Add(folder.Path);
  884. lib.Close();
  885. }
  886. return ret;
  887. }
  888. /// <summary>
  889. /// Creates the forest <see cref="sourceForest"/> using
  890. /// the sources in PropertiesWindow.
  891. /// </summary>
  892. private static List<SourceTree> GetSourceForest()
  893. {
  894. List<SourceTree> forest = new List<SourceTree>();
  895. foreach (SourceData source in SettingsManager.Sources)
  896. {
  897. if (source.Type == SourceType.Library)
  898. foreach (String folder in GetLibraryFolders(source.Data))
  899. InsertSourceTree(new SourceTree() { Data = folder, Ignore = source.Ignore, Children = new List<SourceTree>() }, forest);
  900. else
  901. InsertSourceTree(new SourceTree() { Data = source.Data, Ignore = source.Ignore, Children = new List<SourceTree>() }, forest);
  902. }
  903. return forest;
  904. }
  905. /// <summary>
  906. /// Insert a source tree into a forest. The tree will be inserted into the forest so that
  907. /// the parent will be a folder which contains the folder that the tree describes.
  908. /// </summary>
  909. /// <param name="tree">The tree to insert</param>
  910. /// <param name="forest">The forest to insert the tree into</param>
  911. private static void InsertSourceTree(SourceTree tree, List<SourceTree> forest)
  912. {
  913. for (int i=0; i<forest.Count; i++)
  914. {
  915. SourceTree t = forest[i];
  916. if (tree.Data == t.Data)
  917. return;
  918. // insert tree into t
  919. else if (tree.Data.StartsWith(t.Data))
  920. {
  921. InsertSourceTree(tree, t.Children);
  922. return;
  923. }
  924. // swap t for tree and make t a child of tree
  925. else if (t.Data.StartsWith(tree.Data))
  926. {
  927. tree.Children.Add(t);
  928. forest.RemoveAt(i);
  929. forest.Insert(i, tree);
  930. // continue through the forest and find any additional children for tree
  931. for (int j = i + 1; j < forest.Count; j++)
  932. {
  933. t = forest[j];
  934. if (t.Data.StartsWith(tree.Data))
  935. {
  936. tree.Children.Add(t);
  937. forest.RemoveAt(j--);
  938. }
  939. }
  940. return;
  941. }
  942. }
  943. // insert tree into forest as top tree
  944. forest.Add(tree);
  945. }
  946. /// <summary>
  947. /// Remove a source tree from a forest
  948. /// </summary>
  949. /// <param name="tree">The data of the tree to be removed</param>
  950. /// <param name="forest">The forest to remove the tree from</param>
  951. private static void RemoveSourceTree(String tree, List<SourceTree> forest)
  952. {
  953. // iterate through the forest and find the tree's position
  954. for (int i=0; i < forest.Count; i++)
  955. {
  956. SourceTree t = forest[i];
  957. // found the tree inside the forest
  958. if (t.Data == tree)
  959. {
  960. // put all children into the parent
  961. foreach (SourceTree child in t.Children)
  962. forest.Add(child);
  963. forest.RemoveAt(i);
  964. return;
  965. }
  966. // found a parent of the tree
  967. else if (t.Data.StartsWith(tree))
  968. {
  969. RemoveSourceTree(tree, t.Children);
  970. return;
  971. }
  972. }
  973. }
  974. /// <summary>
  975. /// Scans for Windows 7 Libraries of type Music and adds them to the collection.
  976. /// This is called from ScanLibraries() in either a new thread or in callers thread.
  977. /// </summary>
  978. /// <param name="ScanSourcesWhenDone">Set to true to scan the folders afterwards</param>
  979. private static void ScanLibrariesFunc(bool ScanSourcesWhenDone = false)
  980. {
  981. String librariesPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
  982. + @"\AppData\Roaming\Microsoft\Windows\Libraries\";
  983. // remove non-existing libraries from sources
  984. for (int i = 0; i < SettingsManager.Sources.Count; i++)
  985. if (SettingsManager.Sources[i].Type == SourceType.Library &&
  986. !System.IO.File.Exists(librariesPath + SettingsManager.Sources[i].Data + ".library-ms"))
  987. SettingsManager.Sources.RemoveAt(i--);
  988. // add newly created libraries
  989. DirectoryInfo librariesInfo = new DirectoryInfo(librariesPath);
  990. FileInfo[] libraries = librariesInfo.GetFiles("*.library-ms");
  991. foreach (FileInfo libraryInfo in libraries)
  992. {
  993. String libraryName = System.IO.Path.GetFileNameWithoutExtension(libraryInfo.FullName);
  994. ShellLibrary library = ShellLibrary.Load(libraryName, librariesPath, true);
  995. // newly created libraries will not get a LibraryType so we get an exception here
  996. try
  997. {
  998. // make sure that the library is added
  999. if (library.LibraryType == LibraryFolderType.Music)
  1000. {
  1001. bool AlreadyAdded = false;
  1002. foreach (SourceData src in SettingsManager.Sources)
  1003. {
  1004. if (src.Data == libraryName && src.Type == SourceType.Library)
  1005. {
  1006. AlreadyAdded = true;
  1007. break;
  1008. }
  1009. }
  1010. if (!AlreadyAdded)
  1011. {
  1012. SourceData s = new SourceData();
  1013. s.Data = libraryName;
  1014. s.HumanType = U.T("SourcesTypeLibrary");
  1015. s.Type = SourceType.Library;
  1016. s.Include = true;
  1017. s.Icon = "pack://application:,,,/Platform/Windows/GUI/Images/Icons/Library.ico";
  1018. AddSource(s);
  1019. }
  1020. }
  1021. else // remove the library if neccessary
  1022. {
  1023. for (int i = 0; i < SettingsManager.Sources.Count; i++)
  1024. if (SettingsManager.Sources[i].Data == libraryName && SettingsManager.Sources[i].Type == SourceType.Library)
  1025. SettingsManager.Sources.RemoveAt(i--);
  1026. }
  1027. }
  1028. catch
  1029. {
  1030. // the library was (probably) newly created and didn't get a LibraryType, so we ignore it
  1031. }
  1032. }
  1033. if (ScanSourcesWhenDone) ScanSources();
  1034. }
  1035. /// <summary>
  1036. /// Calculates the difference between two source forests
  1037. /// </summary>
  1038. /// <param name="forest1">The original forest</param>
  1039. /// <param name="forest2">The updated forest</param>
  1040. /// <param name="added">A list which will after calculation contain all trees in forest2 and not forest1</param>
  1041. /// <param name="removed">A list which will after calculation contain all trees in forest1 and not forest2</param>
  1042. private static void GetForestDifference(List<SourceTree> forest1, List<SourceTree> forest2, List<SourceTree> added, List<SourceTree> removed)
  1043. {
  1044. // find all trees that are in forest1 but not forest2
  1045. foreach (SourceTree tree1 in forest1)
  1046. {
  1047. bool isRemoved = true;
  1048. foreach (SourceTree tree2 in forest2)
  1049. {
  1050. if (tree2.Data == tree1.Data)
  1051. {
  1052. GetForestDifference(tree1.Children, tree2.Children, added, removed);
  1053. isRemoved = false;
  1054. if (tree2.Ignore && tree1.Include && !isRemoved)
  1055. added.Add(tree1);
  1056. else if (tree2.Include && tree1.Ignore && !isRemoved)
  1057. removed.Add(tree1);
  1058. break;
  1059. }
  1060. }
  1061. if (isRemoved)
  1062. removed.Add(tree1);
  1063. }
  1064. // find all trees that are in forest2 but not forest1
  1065. foreach (SourceTree tree2 in forest2)
  1066. {
  1067. bool isAdded = true;
  1068. foreach (SourceTree tree1 in forest1)
  1069. {
  1070. if (tree2.Data == tree1.Data)
  1071. {
  1072. isAdded = false;
  1073. break;
  1074. }
  1075. }
  1076. if (isAdded)
  1077. added.Add(tree2);
  1078. }
  1079. }
  1080. #endregion
  1081. #region Event handlers
  1082. /// <summary>
  1083. /// Invoked when a property of the settings manager changes
  1084. /// </summary>
  1085. /// <param name="sender">The sender of the event</param>
  1086. /// <param name="e">The event data</param>
  1087. private static void SettingsManager_PropertyChanged(object sender, PropertyChangedWithValuesEventArgs e)
  1088. {
  1089. switch (e.PropertyName)
  1090. {
  1091. case "Sources":
  1092. ScanSources();
  1093. break;
  1094. }
  1095. }
  1096. /// <summary>
  1097. /// Event handler that gets called when a Library has been renamed
  1098. /// </summary>
  1099. /// <param name="sender">The sender of the event</param>
  1100. /// <param name="e">The event data</param>
  1101. private static void Janitor_LibraryRenamed(object sender, RenamedEventArgs e)
  1102. {
  1103. string oldName = System.IO.Path.GetFileNameWithoutExtension(e.OldName);
  1104. string newName = System.IO.Path.GetFileNameWithoutExtension(e.Name);
  1105. foreach (JanitorTask jt in janitorTasks)
  1106. {
  1107. // on delete this event will fire afterwards, but we should skip it
  1108. if (jt.Job == JanitorJob.DeleteLibrary && jt.Data == newName)
  1109. return;
  1110. // check if this job is already in the queue
  1111. if (jt.Job == JanitorJob.RenameLibrary && jt.Data == oldName + "\n" + newName)
  1112. return;
  1113. }
  1114. if (janitorLazyness != null)
  1115. janitorLazyness.Dispose();
  1116. janitorTasks.Add(new JanitorTask() { Data = oldName + "\n" + newName, Job = JanitorJob.RenameLibrary });
  1117. janitorLazyness = new Timer(Janitor_Clean, null, 400, 3600);
  1118. }
  1119. /// <summary>
  1120. /// Event handler that gets called when a Library has been modified
  1121. /// </summary>
  1122. /// <param name="sender">The sender of the event</param>
  1123. /// <param name="e">The event data</param>
  1124. private static void Janitor_LibraryChanged(object sender, FileSystemEventArgs e)
  1125. {
  1126. JanitorTask task = new JanitorTask() { Data = System.IO.Path.GetFileNameWithoutExtension(e.Name) };
  1127. if (e.ChangeType == WatcherChangeTypes.Deleted)
  1128. task.Job = JanitorJob.DeleteLibrary;
  1129. else if (e.ChangeType == WatcherChangeTypes.Created)
  1130. task.Job = JanitorJob.CreateLibrary;
  1131. else if (e.ChangeType == WatcherChangeTypes.Changed)
  1132. task.Job = JanitorJob.UpdateLibrary;
  1133. else
  1134. return;
  1135. foreach (JanitorTask jt in janitorTasks)
  1136. {
  1137. // on rename and create this event will fire afterwards, but we should skip it
  1138. if (e.ChangeType == WatcherChangeTypes.Changed &&
  1139. ((jt.Job == JanitorJob.RenameLibrary && jt.Data.EndsWith("\n" + System.IO.Path.GetFileNameWithoutExtension(e.Name))) ||
  1140. (jt.Job == JanitorJob.CreateLibrary && jt.Data == System.IO.Path.GetFileNameWithoutExtension(e.Name))))
  1141. {
  1142. return;
  1143. }
  1144. // check if this job is already in the queue
  1145. if (jt.Job == task.Job && jt.Data == task.Data)
  1146. {
  1147. return;
  1148. }
  1149. // an update may be preceded by a delete, so we change the delete task into an update task
  1150. if (task.Job == JanitorJob.UpdateLibrary && jt.Job == JanitorJob.DeleteLibrary && task.Data == jt.Data)
  1151. {
  1152. jt.Job = JanitorJob.UpdateLibrary;
  1153. return;
  1154. }
  1155. // an update may be followed by a delete, so we skip the delete
  1156. if (task.Job == JanitorJob.DeleteLibrary && jt.Job == JanitorJob.UpdateLibrary && task.Data == jt.Data)
  1157. {
  1158. return;
  1159. }
  1160. }
  1161. if (janitorLazyness != null)
  1162. janitorLazyness.Dispose();
  1163. janitorTasks.Add(task);
  1164. janitorLazyness = new Timer(Janitor_Clean, null, 400, 3600);
  1165. }
  1166. /// <summary>
  1167. /// Event handler that gets called when a file has been renamed
  1168. /// </summary>
  1169. /// <param name="sender">The sender of the event</param>
  1170. /// <param name="e">The event data</param>
  1171. private static void Janitor_FileRenamed(object sender, RenamedEventArgs e)
  1172. {
  1173. bool isDir = Directory.Exists(e.FullPath);
  1174. if (!isDir && !MediaManager.IsSupported(e.FullPath))
  1175. return;
  1176. JanitorTask task = new JanitorTask() { Data = e.OldFullPath + "\n" + e.FullPath, Job = isDir ? JanitorJob.RenameFolder : JanitorJob.RenameFile };
  1177. foreach (JanitorTask jt in janitorTasks)
  1178. {
  1179. // on delete this event will fire afterwards, but we should skip it
  1180. if (jt.Job == JanitorJob.DeleteFile && jt.Data == e.FullPath)
  1181. return;
  1182. // check if this job is already in the queue
  1183. if (jt.Job == task.Job && jt.Data == task.Data)
  1184. return;
  1185. }
  1186. //janitorLazyness.Stop();
  1187. if (janitorLazyness != null)
  1188. janitorLazyness.Dispose();
  1189. janitorTasks.Add(task);
  1190. janitorLazyness = new Timer(Janitor_Clean, null, 400, 3600);
  1191. }
  1192. /// <summary>
  1193. /// Event handler that gets called when a file has been modified
  1194. /// </summary>
  1195. /// <param name="sender">The sender of the event</param>
  1196. /// <param name="e">The event data</param>
  1197. private static void Janitor_FileChanged(object sender, FileSystemEventArgs e)
  1198. {
  1199. bool isDir = Directory.Exists(e.FullPath);
  1200. if (!PathIsAdded(e.FullPath) || !MediaManager.IsSupported(e.FullPath))
  1201. return;
  1202. JanitorTask task = new JanitorTask() { Data = e.FullPath };
  1203. if (e.ChangeType == WatcherChangeTypes.Deleted)
  1204. {
  1205. if (MediaManager.IsSupported(e.FullPath))
  1206. task.Job = JanitorJob.DeleteFile;
  1207. else if (Directory.Exists(e.FullPath))
  1208. task.Job = JanitorJob.DeleteFolder;
  1209. else
  1210. return;
  1211. }
  1212. else if (e.ChangeType == WatcherChangeTypes.Created)
  1213. task.Job = isDir ? JanitorJob.CreateFolder : JanitorJob.CreateFile;
  1214. else if (e.ChangeType == WatcherChangeTypes.Changed)
  1215. {
  1216. if (isDir) return;
  1217. task.Job = JanitorJob.UpdateFile;
  1218. }
  1219. else
  1220. return;
  1221. foreach (JanitorTask jt in janitorTasks)
  1222. {
  1223. // on create folder this event will fire afterwards, but we should skip it
  1224. if (!isDir && jt.Job == JanitorJob.CreateFolder && e.ChangeType == WatcherChangeTypes.Changed && jt.Data == e.FullPath)
  1225. return;
  1226. // on rename and create this event will fire afterwards, but we should skip it
  1227. if (e.ChangeType == WatcherChangeTypes.Changed &&
  1228. ((jt.Job == JanitorJob.RenameFile && jt.Data.EndsWith("\n" + e.FullPath)) ||
  1229. (jt.Job == JanitorJob.CreateFile && jt.Data == e.FullPath)))
  1230. return;
  1231. // check if this job is already in the queue
  1232. if (jt.Job == task.Job && jt.Data == task.Data)
  1233. return;
  1234. }
  1235. if (janitorLazyness != null)
  1236. janitorLazyness.Dispose();
  1237. janitorTasks.Add(task);
  1238. janitorLazyness = new Timer(Janitor_Clean, null, 400, 3600);
  1239. }
  1240. /// <summary>
  1241. /// Performs the actual cleaning of the janitor.
  1242. /// This is called from a timer because when a file is modified several events will fire but
  1243. /// we only need to act once.
  1244. ///
  1245. /// This method will take action depending on what have happened, keeping the collection in
  1246. /// sync with the filesystem.
  1247. /// </summary>
  1248. /// <param name="state">The timer state</param>
  1249. private static void Janitor_Clean(object state)
  1250. {
  1251. janitorLazyness.Dispose();
  1252. janitorLazyness = null;
  1253. // copy tasks and clear the list in case another event is fired while we are working
  1254. List<JanitorTask> tasks = new List<JanitorTask>(janitorTasks);
  1255. janitorTasks.Clear();
  1256. foreach (JanitorTask task in tasks)
  1257. {
  1258. SourceData s = null;
  1259. switch (task.Job)
  1260. {
  1261. case JanitorJob.CreateFile:
  1262. AddFile(task.Data, true);
  1263. break;
  1264. case JanitorJob.DeleteFile:
  1265. RemoveFile(task.Data);
  1266. break;
  1267. case JanitorJob.CreateLibrary:
  1268. try
  1269. {
  1270. ShellLibrary library = ShellLibrary.Load(task.Data, librariesPath, true);
  1271. s = GetSourceData(task.Data);
  1272. if (library.LibraryType == LibraryFolderType.Music && s == null)
  1273. AddSource(new SourceData
  1274. {
  1275. Data = task.Data,
  1276. Include = true,
  1277. HumanType = U.T("SourcesTypeLibrary"),
  1278. Type = SourceType.Library
  1279. });
  1280. }
  1281. catch (Exception exc)
  1282. {
  1283. U.L(LogLevel.Warning, "FILESYSTEM", "Could not read newly created Library " + task.Data + ": " + exc.Message);
  1284. }
  1285. break;
  1286. case JanitorJob.CreateFolder:
  1287. case JanitorJob.DeleteFolder:
  1288. ScanSources();
  1289. break;
  1290. case JanitorJob.DeleteLibrary:
  1291. s = GetSourceData(task.Data);
  1292. if (s != null)
  1293. RemoveSource(s);
  1294. break;
  1295. case JanitorJob.RenameFolder:
  1296. case JanitorJob.RenameFile:
  1297. string[] names = task.Data.Split('\n');
  1298. RenamePath(names[0], names[1], SettingsManager.FileTracks);
  1299. RenamePath(names[0], names[1], SettingsManager.QueueTracks);
  1300. RenamePath(names[0], names[1], SettingsManager.HistoryTracks);
  1301. foreach (PlaylistData p in SettingsManager.Playlists)
  1302. RenamePath(names[0], names[1], p.Tracks);
  1303. break;
  1304. case JanitorJob.RenameLibrary:
  1305. string[] lnames = task.Data.Split('\n');
  1306. String oldLibraryName = System.IO.Path.GetFileNameWithoutExtension(lnames[0]);
  1307. String newLibraryName = System.IO.Path.GetFileNameWithoutExtension(lnames[1]);
  1308. s = GetSourceData(oldLibraryName);
  1309. if (s != null)
  1310. s.Data = newLibraryName;
  1311. break;
  1312. case JanitorJob.UpdateFile:
  1313. DispatchPathModified(task.Data);
  1314. break;
  1315. case JanitorJob.UpdateLibrary:
  1316. ShellLibrary lib = ShellLibrary.Load(task.Data, librariesPath, true);
  1317. s = GetSourceData(task.Data);
  1318. // remove if type changed to non-music
  1319. if (s != null && lib.LibraryType != LibraryFolderType.Music)
  1320. RemoveSource(s);
  1321. // if still type music, detect changed folders
  1322. else if (s != null)
  1323. ScanLibraries(true);
  1324. // add if changed to type music
  1325. else if (lib.LibraryType == LibraryFolderType.Music)
  1326. AddSource(new SourceData
  1327. {
  1328. Data = task.Data,
  1329. Type = SourceType.Library,
  1330. HumanType = U.T("SourcesTypeLibrary"),
  1331. Include = true
  1332. });
  1333. break;
  1334. default: // we really shouldn't ever get here
  1335. return;
  1336. }
  1337. }
  1338. }
  1339. /// <summary>
  1340. /// Invoked when something happens to a track.
  1341. /// </summary>
  1342. /// <param name="sender">Sender of the event</param>
  1343. /// <param name="e">Event arguments</param>
  1344. public static void Track_PropertyChanged(object sender, PropertyChangedEventArgs e)
  1345. {
  1346. TrackData track = sender as TrackData;
  1347. DispatchTrackModified(track, e);
  1348. }
  1349. #endregion
  1350. #region Dispatchers
  1351. /// <summary>
  1352. /// The dispatcher of the <see cref="FilesystemManager.SourcesModified"/> event
  1353. /// </summary>
  1354. /// <param name="track">The track that was either added, removed or updated</param>
  1355. /// <param name="modType">The modification type of the source</param>
  1356. /// <param name="callback">A function that will be sent along with the SourceModified event</param>
  1357. /// <param name="callbackParams">The parameters for the callback function</param>
  1358. private static void DispatchSourceModified(TrackData track, SourceModificationType modType, ScannerCallback callback = null, object callbackParams = null)
  1359. {
  1360. if (SourceModified != null)
  1361. SourceModified(null, new SourceModifiedEventArgs(track, modType, callback, callbackParams));
  1362. }
  1363. /// <summary>
  1364. /// The dispatcher of the <see cref="SourceAdded"/> event
  1365. /// </summary>
  1366. /// <param name="source">The source that was added</param>
  1367. private static void DispatchSourceAdded(SourceData source)
  1368. {
  1369. if (SourceAdded != null)
  1370. SourceAdded(null, new SourcesModifiedEventArgs(source));
  1371. }
  1372. /// <summary>
  1373. /// The dispatcher of the <see cref="SourceRemoved"/> event
  1374. /// </summary>
  1375. /// <param name="source">The source that was removed</param>
  1376. private static void DispatchSourceRemoved(SourceData source)
  1377. {
  1378. if (SourceRemoved != null)
  1379. SourceRemoved(null, new SourcesModifiedEventArgs(source));
  1380. }
  1381. /// <summary>
  1382. /// The dispatcher of the <see cref="TrackModified"/> event
  1383. /// </summary>
  1384. /// <param name="track">The track that was modified</param>
  1385. /// <param name="e">The event data</param>
  1386. private static void DispatchTrackModified(TrackData track, PropertyChangedEventArgs e)
  1387. {
  1388. if (TrackModified != null)
  1389. TrackModified(track, e);
  1390. }
  1391. /// <summary>
  1392. /// The dispatcher of the <see cref="PathRenamed"/> event
  1393. /// </summary>
  1394. /// <param name="oldName">The old name of the path</param>
  1395. /// <param name="newName">The new name of the path</param>
  1396. private static void DispatchPathRenamed(String oldName, String newName)
  1397. {
  1398. if (PathRenamed != null)
  1399. PathRenamed(null, new RenamedEventArgs(WatcherChangeTypes.Renamed, null, newName, oldName));
  1400. }
  1401. /// <summary>
  1402. /// The dispatcher of the <see cref="PathModified"/> event
  1403. /// </summary>
  1404. /// <param name="path">The path that was modified</param>
  1405. private static void DispatchPathModified(string path)
  1406. {
  1407. if (PathModified != null)
  1408. PathModified(null, new PathModifiedEventArgs(path));
  1409. }
  1410. /// <summary>
  1411. /// The dispatcher of the <see cref="ProgressChanged"/> event
  1412. /// </summary>
  1413. /// <param name="progressPercentage">The percentage of the current progress</param>
  1414. /// <param name="state">The state of the progress</param>
  1415. private static void DispatchProgressChanged(int progressPercentage, string state)
  1416. {
  1417. if (ProgressChanged != null)
  1418. ProgressChanged(null, new ProgressChangedEventArgs(progressPercentage, (object)state));
  1419. }
  1420. #endregion
  1421. #endregion
  1422. #region Events
  1423. /// <summary>
  1424. /// Occurs when a track has been modified
  1425. /// </summary>
  1426. public static event PropertyChangedEventHandler TrackModified;
  1427. /// <summary>
  1428. /// Occurs when a path has been renamed
  1429. /// </summary>
  1430. public static event RenamedEventHandler PathRenamed;
  1431. /// <summary>
  1432. /// Occurs when a path has been modified
  1433. /// </summary>
  1434. public static event PathModifiedEventHandler PathModified;
  1435. /// <summary>
  1436. /// Occurs when progress of a scan has changed
  1437. /// </summary>
  1438. public static event ProgressChangedEventHandler ProgressChanged;
  1439. /// <summary>
  1440. /// Occurs when a source has been modified
  1441. /// </summary>
  1442. public static event SourceModifiedEventHandler SourceModified;
  1443. /// <summary>
  1444. /// Occurs when a source has been added
  1445. /// </summary>
  1446. public static event SourcesModifiedEventHandler SourceAdded;
  1447. /// <summary>
  1448. /// Occurs when a source has been removed
  1449. /// </summary>
  1450. public static event SourcesModifiedEventHandler SourceRemoved;
  1451. #endregion Events
  1452. }
  1453. #region Delegates
  1454. /// <summary>
  1455. /// Represents the method that will handle the <see cref="FilesystemManager.PathModified"/> event.
  1456. /// </summary>
  1457. /// <param name="sender">The sender of the event</param>
  1458. /// <param name="e">The event data</param>
  1459. public delegate void PathModifiedEventHandler(object sender, PathModifiedEventArgs e);
  1460. /// <summary>
  1461. /// Represents the method that will handle the <see cref="FilesystemManager.SourcesModified"/> event.
  1462. /// </summary>
  1463. /// <param name="sender">The sender of the event</param>
  1464. /// <param name="e">The event data</param>
  1465. public delegate void SourceModifiedEventHandler(object sender, SourceModifiedEventArgs e);
  1466. /// <summary>
  1467. /// Represents the method that will handle the <see cref="FilesystemManager.SourceAdded"/> or <see cref="FilesystemManager.SourceRemoved"/> event.
  1468. /// </summary>
  1469. /// <param name="sender">The sender of the event</param>
  1470. /// <param name="e">The event data</param>
  1471. public delegate void SourcesModifiedEventHandler(object sender, SourcesModifiedEventArgs e);
  1472. /// <summary>
  1473. /// Represents the method that will be called when the scanner is finished.
  1474. /// </summary>
  1475. /// <param name="param">
  1476. /// A list of three objects:
  1477. /// * The ViewDetails that was dropped upon
  1478. /// * The FileDropEventArgs containing the event data for the drop
  1479. /// * The string of the path that was scanned
  1480. /// </param>
  1481. public delegate void ScannerCallback(object param);
  1482. #endregion Delegates
  1483. /// <summary>
  1484. /// The task which a Janitor should perform
  1485. /// </summary>
  1486. public class JanitorTask
  1487. {
  1488. #region Properties
  1489. /// <summary>
  1490. /// The specific job that the Janitor should perform
  1491. /// </summary>
  1492. public JanitorJob Job { get; set; }
  1493. /// <summary>
  1494. /// The filename, path or library name on which the task should be performed
  1495. /// </summary>
  1496. public String Data { get; set; }
  1497. #endregion
  1498. #region Methods
  1499. /// <summary>
  1500. /// Prints the job as a string
  1501. /// </summary>
  1502. /// <returns>A string describing the job</returns>
  1503. public override string ToString()
  1504. {
  1505. return String.Format("'{0}' on {1}", Job, Data);
  1506. }
  1507. #endregion
  1508. }
  1509. /// <summary>
  1510. /// The job of a janitor.
  1511. /// This is used by a janitor to know what it should do when Clean() is called.
  1512. /// </summary>
  1513. public enum JanitorJob
  1514. {
  1515. /// <summary>
  1516. /// A new Windows 7 Library has been created and needs to be scanned
  1517. /// </summary>
  1518. CreateLibrary,
  1519. /// <summary>
  1520. /// A Windows 7 Library was deleted
  1521. /// </summary>
  1522. DeleteLibrary,
  1523. /// <summary>
  1524. /// A Windows 7 Library was renamed
  1525. /// </summary>
  1526. RenameLibrary,
  1527. /// <summary>
  1528. /// A Windows 7 Library was changed and needs to be rescanned
  1529. /// </summary>
  1530. UpdateLibrary,
  1531. /// <summary>
  1532. /// A new file was created inside a watched folder
  1533. /// </summary>
  1534. CreateFile,
  1535. /// <summary>
  1536. /// A file was removed from a watched folder
  1537. /// </summary>
  1538. DeleteFile,
  1539. /// <summary>
  1540. /// A file was renamed inside a watched folder
  1541. /// </summary>
  1542. RenameFile,
  1543. /// <summary>
  1544. /// A file inside a watched folder was updated
  1545. /// </summary>
  1546. UpdateFile,
  1547. /// <summary>
  1548. /// A folder that resides inside a watched folder was created
  1549. /// </summary>
  1550. CreateFolder,
  1551. /// <summary>
  1552. /// A folder that resides inside a watched folder was removed
  1553. /// </summary>
  1554. DeleteFolder,
  1555. /// <summary>
  1556. /// A folder that resides inside a watched folder was renamed
  1557. /// </summary>
  1558. RenameFolder
  1559. }
  1560. /// <summary>
  1561. /// The way that the source was modified
  1562. /// </summary>
  1563. public enum SourceModificationType
  1564. {
  1565. /// <summary>
  1566. /// A track was added
  1567. /// </summary>
  1568. Added,
  1569. /// <summary>
  1570. /// A track was removed
  1571. /// </summary>
  1572. Removed,
  1573. /// <summary>
  1574. /// A track was updated
  1575. /// </summary>
  1576. Updated
  1577. }
  1578. /// <summary>
  1579. /// A source where Stoffi should look for music
  1580. /// </summary>
  1581. public class SourceTree
  1582. {
  1583. #region Properties
  1584. /// <summary>
  1585. /// A list of child sources (only applicable on folders)
  1586. /// </summary>
  1587. public List<SourceTree> Children { get; set; }
  1588. /// <summary>
  1589. /// The filename or path
  1590. /// </summary>
  1591. public String Data { get; set; }
  1592. /// <summary>
  1593. /// Whether the file (or contents of the folder) should be included in the library
  1594. /// </summary>
  1595. public Boolean Include { get; set; }
  1596. /// <summary>
  1597. /// Whether the file (or contents of the folder) should not be included in the library
  1598. /// </summary>
  1599. public Boolean Ignore { get { return !Include; } set { Include = !value; } }
  1600. #endregion
  1601. }
  1602. #region Event arguments
  1603. /// <summary>
  1604. /// Provides data for the <see cref="FilesystemManager.SourceModified"/> event
  1605. /// </summary>
  1606. public class SourceModifiedEventArgs : EventArgs
  1607. {
  1608. #region Properties
  1609. /// <summary>
  1610. /// Gets the track that was either added or removed
  1611. /// </summary>
  1612. public TrackData Track { get; private set; }
  1613. /// <summary>
  1614. /// Gets the modification type of the source
  1615. /// </summary>
  1616. public SourceModificationType ModificationType { get; private set; }
  1617. /// <summary>
  1618. ///
  1619. /// </summary>
  1620. public ScannerCallback Callback { get; private set; }
  1621. /// <summary>
  1622. ///
  1623. /// </summary>
  1624. public object CallbackParams { get; private set; }
  1625. #endregion
  1626. #region Constructor
  1627. /// <summary>
  1628. /// Initializes a new instance of the <see cref="SourcesModifiedEventArgs"/> class
  1629. /// </summary>
  1630. /// <param name="track">The track that was either added or removed</param>
  1631. /// <param name="modType">The modification type of the source</param>
  1632. /// <param name="callback">A function to call when the event is handled</param>
  1633. /// <param name="callbackParams">The parameters to the callback function</param>
  1634. public SourceModifiedEventArgs(TrackData track, SourceModificationType modType, ScannerCallback callback = null, object callbackParams = null)
  1635. {
  1636. Track = track;
  1637. ModificationType = modType;
  1638. Callback = callback;
  1639. CallbackParams = callbackParams;
  1640. }
  1641. #endregion
  1642. }
  1643. /// <summary>
  1644. /// Provides data for the <see cref="FilesystemManager.SourceAdded"/> or <see cref="FilesystemManager.SourceRemoved"/> event
  1645. /// </summary>
  1646. public class SourcesModifiedEventArgs : EventArgs
  1647. {
  1648. #region Properties
  1649. /// <summary>
  1650. /// Gets the source that was modified
  1651. /// </summary>
  1652. public SourceData Source { get; private set; }
  1653. #endregion
  1654. #region Constructor
  1655. /// <summary>
  1656. /// Initializes a new instance of the <see cref="SourcesModifiedEventArgs"/> class
  1657. /// </summary>
  1658. /// <param name="source">The source that was modified</param>
  1659. public SourcesModifiedEventArgs(SourceData source)
  1660. {
  1661. Source = source;
  1662. }
  1663. #endregion
  1664. }
  1665. /// <summary>
  1666. /// Provides data for the <see cref="FilesystemManager.PathModified"/> event
  1667. /// </summary>
  1668. public class PathModifiedEventArgs : EventArgs
  1669. {
  1670. #region Properties
  1671. /// <summary>
  1672. /// Gets the path that was modified
  1673. /// </summary>
  1674. public String Path { get; private set; }
  1675. #endregion
  1676. #region Constructor
  1677. /// <summary>
  1678. /// Initializes a new instance of the <see cref="PathModifiedEventArgs"/> class
  1679. /// </summary>
  1680. /// <param name="path">The path that was modified</param>
  1681. public PathModifiedEventArgs(String path)
  1682. {
  1683. Path = path;
  1684. }
  1685. #endregion
  1686. }
  1687. #endregion
  1688. }