65 | Enable Download History 66 |
67 |├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── appveyor.yml ├── docs ├── ffmpeg.md └── index.md ├── readme.md └── src ├── .gitignore ├── appicon.icns ├── appicon.ico ├── appicon.png ├── build ├── appicon.icns ├── appicon.ico └── appicon.png ├── custom_modules ├── DataManager │ └── index.js ├── DownloadManager │ └── index.js └── Favorites │ └── index.js ├── dist └── empty.txt ├── index.js ├── lmt ├── appicon.png ├── chat.html ├── fans.html ├── favorites-list.html ├── following.html ├── fonts │ ├── roboto │ │ ├── Roboto-Bold.woff │ │ ├── Roboto-Bold.woff2 │ │ ├── Roboto-Light.woff │ │ ├── Roboto-Light.woff2 │ │ ├── Roboto-Medium.woff │ │ ├── Roboto-Medium.woff2 │ │ ├── Roboto-Regular.woff │ │ ├── Roboto-Regular.woff2 │ │ ├── Roboto-Thin.woff │ │ └── Roboto-Thin.woff2 │ └── sourcesans │ │ ├── sourcecodepro-regular-webfont.eot │ │ ├── sourcecodepro-regular-webfont.svg │ │ ├── sourcecodepro-regular-webfont.ttf │ │ ├── sourcecodepro-regular-webfont.woff │ │ ├── sourcesanspro-light-webfont.eot │ │ ├── sourcesanspro-light-webfont.svg │ │ ├── sourcesanspro-light-webfont.ttf │ │ ├── sourcesanspro-light-webfont.woff │ │ ├── sourcesanspro-regular-webfont.eot │ │ ├── sourcesanspro-regular-webfont.svg │ │ ├── sourcesanspro-regular-webfont.ttf │ │ ├── sourcesanspro-regular-webfont.woff │ │ ├── sourcesanspro-semibold-webfont.eot │ │ ├── sourcesanspro-semibold-webfont.svg │ │ ├── sourcesanspro-semibold-webfont.ttf │ │ ├── sourcesanspro-semibold-webfont.woff │ │ └── stylesheet.css ├── images │ ├── blank.png │ ├── drop-down-triangle-dark.png │ ├── ic_add_circle_outline_white_24px.svg │ ├── ic_arrow_drop_down_white_18px.svg │ ├── ic_book_white_24px.svg │ ├── ic_cancel_white_24px.svg │ ├── ic_chat_white_24px.svg │ ├── ic_check_white_24px.svg │ ├── ic_close_white_24px.svg │ ├── ic_content_copy_white_18px.svg │ ├── ic_content_copy_white_24px.svg │ ├── ic_favorite_border_white_24px.svg │ ├── ic_favorite_white_24px.svg │ ├── ic_file_download_white_24px.svg │ ├── ic_file_upload_white_24px.svg │ ├── ic_fullscreen_white_18px.svg │ ├── ic_menu_white_24px.svg │ ├── ic_pause_circle_outline_white_24px.svg │ ├── ic_pause_white_18px.svg │ ├── ic_play_arrow_white_18px.svg │ ├── ic_play_circle_outline_white_24px.svg │ ├── ic_search_white_18px.svg │ ├── ic_settings_white_24px.svg │ ├── liveme.tools.icon.svg │ ├── search.svg │ └── wait.gif ├── index.html ├── javascript │ ├── cclist.js │ ├── favorites.js │ ├── hls.js │ ├── jquery-3.2.1.min.js │ ├── livemetools.js │ └── settings.js ├── livemeomg.html ├── player.html ├── queue.html ├── settings.html ├── splash.html ├── style │ └── style.css ├── update.html └── upgrade.html └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | .DS_Store 61 | .DS_Store 62 | src/.DS_Store 63 | 64 | # Specific to our code 65 | *.sh 66 | *.php 67 | 68 | dist/* 69 | 70 | *.lock 71 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode8.3 2 | 3 | dist: trusty 4 | sudo: false 5 | 6 | language: node_js 7 | node_js: "8" 8 | 9 | env: 10 | global: 11 | - ELECTRON_CACHE=$HOME/.cache/electron 12 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 13 | 14 | os: 15 | - linux 16 | - osx 17 | 18 | cache: 19 | directories: 20 | - node_modules 21 | - $HOME/.cache/electron 22 | - $HOME/.cache/electron-builder 23 | - $HOME/.npm/_prebuilds 24 | 25 | before_install: 26 | - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v2.2.0/git-lfs-$([ "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-2.2.0.tar.gz | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull 27 | - export PATH="$HOME/.yarn/bin:$PATH" 28 | - cd src 29 | 30 | install: 31 | - npm install -g yarn 32 | - yarn install 33 | 34 | script: 35 | - yarn dist-travis 36 | 37 | before_cache: 38 | - rm -rf $HOME/.cache/electron-builder/wine 39 | 40 | branches: 41 | except: 42 | - "/^v\\d+\\.\\d+\\.\\d+$/" 43 | 44 | notifications: 45 | email: false 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | ### 7.x.x Releases 4 | 5 | **No more releases are scheduled at this time, please use the stable 6.x.x branch as the 7.x.x branch is buggy still.** 6 | 7 | #### 2017-11-25 - v7.0.8 (EOL) 8 | **Fixed:** 9 | - Addressed multiple crash and other bug issues 10 | - Clean up of database commit code to reduce disk writes 11 | 12 | #### 2017-11-24 - v7.0.7 13 | **Fixed:** 14 | - Addressed bug issues 15 | 16 | #### 2017-11-24 - v7.0.6 17 | **Fixed:** 18 | - Addressed bug issues 19 | 20 | #### 2017-11-23 - v7.0.5 21 | **Fixed:** 22 | - Addressed bug issues 23 | 24 | 25 | #### 2017-11-23 - v7.0.4 26 | 27 | #### 2017-11-21 - v7.0.2 28 | **Fixed:** 29 | - Fixed download issue #111 and issue #107 30 | 31 | #### 2017-11-19 - v7.0.1 32 | **Added:** 33 | - Added settings option to change visited profile timeout with custom options 34 | - Added commands to DataManager module for download tracking (Need to update Downloads Module) 35 | 36 | **Fixed:** 37 | - Updated DataManager module for better handling of lookups and storage formats 38 | - Fixed crash when running first time after upgrading to 7.0.x release. 39 | 40 | #### 2017-11-18 - v7.0.0 41 | **Added:** 42 | - Changed data storage system to using lowDB for easier management and stability 43 | - Added ability to track previously visited UserIDs 44 | - Speed improvements for Favorites window and functions 45 | 46 | ### 6.x.x Releases 47 | 48 | #### 2016-11-17 - v6.3.2 49 | **Fixed:** 50 | - Fixed some minor code bugs 51 | - Fixed autobuild process bugs 52 | 53 | #### 2016-11-17 - v6.3.1 54 | **Fixed:** 55 | - Fixed file name issue in download module causing some playlists to download as `playlist.mp4` or `playlist_eof.mp4` 56 | 57 | #### 2017-11-17 - v6.3.0 58 | **Added:** 59 | - Added new download manager with chunk support speeding update downloads 60 | - Added ability to search Fans and Followings by UserID 61 | 62 | #### 2017-11-06 - v6.2.1 63 | **Fixed:** 64 | - Fixed bug that was causing the main window to not show. 65 | 66 | #### 2017-11-06 - v6.2.0 67 | **Added:** 68 | - Added support for saving and restoring size and position of the main, queue, player and favorite windows. 69 | 70 | **Fixed:** 71 | - Fixed rendering bug when displaying replays that would occasionally occur. 72 | 73 | #### 2017-10-28 - v6.1.4 74 | **Fixed:** 75 | - Thanks to @zp for fixing the duration bug in the video player 76 | 77 | #### 2017-10-28 - v6.1.3 78 | **Fixed:** 79 | - Rebuilt using updated LiveMe-API with fix for VideoID search and Message History issues. 80 | 81 | #### 2017-10-26 - v6.1.2 82 | **Fixed:** 83 | - Updated auto load on scroll to now trigger near the end to avoid issues experienced by some users on Windows 84 | 85 | #### 2017-10-25 - v6.1.1 86 | **Fixed:** 87 | - Fixed broken hashtag search. 88 | 89 | #### 2017-10-24 - v6.1.0 90 | **Added:** 91 | - Added search option to Fans, Followings and Favorites windows 92 | - Added countryCode list to source for future implementation or use 93 | 94 | **Fixed:** 95 | - Cleanup of some of the visual stylings code 96 | 97 | #### 2017-10-20 - v6.0.12 98 | **Added:** 99 | - Updated the player skin to support displaying total video length 100 | - Updated the player progress bar to show a better buffer state and position indicator 101 | 102 | #### 2017-10-17 - v6.0.11 103 | **Fixed:** 104 | - Fixed Video ID List import bug 105 | - Fixed Replay entry for searched video missing playback button 106 | - Fixed overlay status message of main window 107 | 108 | #### 2017-10-15 - v6.0.10 109 | **Fixed:** 110 | - Fixed showing empty fields for Video URL when it wasn't available 111 | - Fixed rendering issue when replays were unavailable when a search was performed. 112 | - Fixed VideoID List import bugs. 113 | 114 | #### 2017-10-15 - v6.0.9 115 | **Fixed:** 116 | - Fixed video search bug 117 | 118 | #### 2017-10-11 - v6.0.8 119 | **Fixed:** 120 | - Fixed layout issue when a full length date is present 121 | - Fixed Favorites Export bug 122 | 123 | #### 2017-10-09 - v6.0.7 124 | **Fixed:** 125 | - Fixed broken Favorites Export function. 126 | 127 | #### 2017-10-01 - v6.0.6 128 | **Added:** 129 | - Updated video progress bar styling for better visibility 130 | - Ffmpeg checks to see if it's available and alert you if it can't be found on startup. 131 | - Added settings: 132 | - You can manually choose a version of ffmpeg for LMT to use. 133 | - A button to check if ffmpeg is valid and can be used. 134 | - Added a basic check to see if the user is currently live. If they aren't, re-enable the download on an alternate link. (If the first one linked to a live url but they weren't live - it wouldn't work) 135 | 136 | **Fixed:** 137 | - Fixed broken time in message history. 138 | - Fixed time jumping from message history when time was clicked on. 139 | - Fixed settings resetting if you changed download directory but didn't save. 140 | - Fixed download directory window not showing. 141 | - Fixed crash if you use File -> Quit. 142 | 143 | **Changed:** 144 | - Bundling Windows versions as a portable version. No extraction required. 145 | - Merged x64 and x86 into one portable version - it will use the x64 version if you're running 64-bit and the x86 version if you're running on 32-bit. 146 | - Color of downloaded video highlight is now a green, instead of a very faint white. 147 | 148 | #### 2017-09-26 - v6.0.5 149 | **Fixed:** 150 | - Fixed broken VideoID search 151 | 152 | #### 2017-09-26 - v6.0.4 153 | **Added:** 154 | - Will show the details of the video that was directly search by VideoID 155 | 156 | #### 2017-09-26 - v6.0.3 157 | **Added:** 158 | - Restored the context (right-click) menu to text fields. 159 | 160 | **Fixed:** 161 | - Removed FFMPEG prompt 162 | - Fixed Username search not returning results at times. 163 | - Fixed replays not showing at times due to all being invisible and not downloaded. 164 | - Fixed closing issue experienced by some users. 165 | 166 | #### 2017-09-25 - v6.0.2 167 | **Fixed:** 168 | - Fixed issue where LiveMeOMG page was sending the VideoID instead of the URL to the player. 169 | 170 | #### 2017-09-25 - v6.0.1 171 | **Fixed:** 172 | - Removed bundled FFMPEG executable due to issues with macOS and Windows systems. 173 | 174 | #### 2017-09-24 - v6.0.0 175 | **Added:** 176 | - Migrated to using LiveMe API module 177 | - Whole new UI styling added 178 | - Added custom video player UI 179 | - Added ability to jump to video time index from message history by click on the time 180 | - Added ability to trigger a search by clicking on the username in the message history 181 | - Added autoload of content when you scroll to the bottom with loading of 10 entries at a time 182 | - Added LiveMe OMG window with ability to watch videos from it and search the users (All, Only Girls, Only Boys) 183 | 184 | **Fixed:** 185 | - Moved List Import and Export functions to main thread 186 | - Major code cleanups 187 | - Upgraded jQuery to 3.2.1 from 2.2.4 188 | - Improved network data speeds by moving all web requests to Node modules from jQuery 189 | 190 | ### 5.x.x Releases 191 | 192 | #### 2017-09-17 - v5.0.9 193 | **Added:** 194 | - Can now delete active downloads as well. 195 | 196 | **Fixed:** 197 | - See commits for details. 198 | 199 | #### 2017-09-13 - v5.0.6 200 | **Added:** 201 | - Automatically will download FFMPEG if its not found on the computer. 202 | 203 | **Fixed:** 204 | - Updated to use only FFMPEG for downloading of playlists. 205 | - Issue #54 206 | - Issue #56 207 | - Issue #57 208 | 209 | #### 2017-09-11 - v5.0.3 210 | **Fixed:** 211 | - Search loop issue 212 | - Minor favorites cleanup 213 | 214 | 215 | #### 2017-09-11 - v5.0.2 216 | **Added:** 217 | - Will show queue window when first download is clicked now. 218 | 219 | **Fixed:** 220 | - Issue #59 221 | - Issue #56 222 | 223 | 224 | #### 2017-09-08 - v5.0.1 225 | **Fixed:** 226 | - Fixed no replays when set to unlimited replays. 227 | 228 | 229 | #### 2017-09-08 - v5.0.0 230 | **Added:** 231 | - Option to empty download queue now available in the queue window 232 | - Ability to add a single URL to download queue 233 | - Ability to limit number of replay results 234 | - Import of a list of VideoIDs 235 | - Export a list of Favorites (UserID) list 236 | - Shows VideoID of each replay next to its URL 237 | 238 | **Fixed:** 239 | - Disabled Live Video download so it doesn't cause a hangup of the queue 240 | - Moved custom modules to main thread to fix multiple instance issues and lost data 241 | - Adding a URL to downloader igored Pause state (See #47) 242 | - Fixed critical download and queue bugs (See #47) 243 | - Fixed critical FFMPEG bug (See #47) 244 | - Fixed URL removal bug (See #47) 245 | 246 | 247 | ### 4.x.x Releases 248 | 249 | #### 2017-09-05 - v4.6.1 250 | **Fixed:** 251 | - Removed download from being re-added back to queue upon failure. 252 | - Added input cleanup when importing a list of URLs to download. 253 | 254 | #### 2017-08-31 - v4.6.0 255 | **Added:** 256 | - Added ability to cancel getting user's replays. 257 | - Minor code improvements. 258 | 259 | **Fixed:** 260 | - Cleanup of code. 261 | - Removed debug code found. 262 | 263 | #### 2017-08-30 - v4.5.0 264 | **Added:** 265 | - Updated favorites list to now show extended info when available. 266 | - Added status text when doing lookups and searches. 267 | - Added bad UID detectors and handlers for lookups. 268 | 269 | **Fixed:** 270 | - Issue #39 271 | - Issue #40 272 | - Typos fixed. 273 | 274 | #### 2017-08-26 - v4.4.2 275 | **Added:** 276 | - Added save_queue to purge_queue function 277 | - Added null detector when closing the app that happens occasionally. 278 | 279 | **Fixed:** 280 | - Removed debug code. 281 | - Minor code cleanup and optimization 282 | 283 | #### 2017-08-25 - v4.4.0 284 | **Added:** 285 | - Issue #37 286 | 287 | #### 2017-08-23 - v4.3.0 288 | **Added:** 289 | - Ability to flush all queue'd download entries from Settings page. 290 | - Ability to refresh the User Avatar and Nickname list in Favorites window 291 | - Some null detectors to help avoid error popping up. 292 | 293 | **Fixed:** 294 | - Issue #31 295 | - Issue #33 296 | - Issue #34 297 | 298 | #### 2017-08-21 - v4.2.0 299 | Minor coding fixes and cleanup. Also fixed the detector bug in the Update notice. 300 | 301 | #### 2017-08-20 - v4.1.5 302 | **Added:** 303 | - Now checks for updated versions availability 304 | 305 | **Fixed:** 306 | - Issue #29 307 | - Issue #31 308 | 309 | #### 2017-08-16 - v4.1.3 310 | **Fixed:** 311 | - Issue #28 312 | 313 | #### 2017-08-16 - v4.1.2 314 | **Fixed:** 315 | - Issue #27 316 | 317 | #### 2017-08-15 - v4.1.1 318 | **Added:** 319 | - Variable height resizing of queue window 320 | 321 | **Fixed:** 322 | - Issue #25 323 | - Issue #26 324 | 325 | #### 2017-08-15 - v4.1.0 326 | **Added:** 327 | - Added button to hide download queue 328 | - Added ability to remove entries from download queue (Issue #24) 329 | 330 | **Fixed:** 331 | - Issue #23 332 | 333 | #### 2017-08-14 - v4.0.0 334 | **Added:** 335 | - New download queue and handler, now supports using FFMPEG 336 | - Ability to enable custom filenaming of downloaded playlists 337 | - Improved list renderings 338 | - Improved user interfaces 339 | - Ability to disable/enable download history tracking 340 | - Auto-recovery of download queue if crashed or closed before they are finished 341 | - and much much more! 342 | 343 | **Fixed:** 344 | - Issue #15 345 | - Issue #16 346 | - Issue #18 347 | - Iusse #20 348 | 349 | 350 | ### 3.x.x Releases 351 | 352 | #### 2017-08-10 - v3.6.0 353 | **Added:** 354 | - polydragon's chat history code 355 | 356 | **Fixed:** 357 | - Couple minor code bugs 358 | 359 | #### 2017-08-10 - v3.5.6 360 | **Added:** 361 | - Button to show the queue window 362 | 363 | **Fixed:** 364 | - An issue where the queue download list would sometimes have a stale entry that would be ignored 365 | 366 | #### 2017-08-10 - v3.5.4 367 | **Fixed:** 368 | - Issue #14 369 | - Minor bug in queue causing entries to be stalled and remain 370 | 371 | #### 2017-08-09 - v3.5.2 372 | **Fixed:** 373 | - Couple minor bugs with favorites 374 | - Fix slowdown of Fans and Following lists not fully loading. 375 | 376 | #### 2017-08-09 - v3.5.1 377 | **Fixed:** 378 | - Bug in favorites storage 379 | 380 | #### 2017-08-09 - v3.5.0 381 | **Added:** 382 | - Now keeps track of what's been downloaded to avoid multiple downloads. 383 | - Queue recovery if the app crashes. 384 | 385 | **Fixed:** 386 | - Issue #10 387 | - Issue #11 388 | - Issue #12 389 | - Issue #13 390 | 391 | ### Prior Releases 392 | **See [Releases](https://github.com/thecoder75/liveme-tools/releases) for details on prior versions.** 393 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | os: unstable 2 | cache: 3 | - node_modules 4 | environment: 5 | GH_TOKEN: 6 | secure: ruXayOUgryDqMg8KBrkEVzK3WZCvh+nJq1ZVA8NRylHdeQQDQO44wfyBf0Bi9ZUt 7 | matrix: 8 | - nodejs_version: 8 9 | install: 10 | - ps: Install-Product node $env:nodejs_version 11 | - set CI=true 12 | - cd src 13 | - yarn install 14 | matrix: 15 | fast_finish: true 16 | build: off 17 | version: '{branch}-{build}' 18 | shallow_clone: true 19 | clone_depth: 1 20 | test_script: 21 | - yarn dist-appveyor -------------------------------------------------------------------------------- /docs/ffmpeg.md: -------------------------------------------------------------------------------- 1 | # Setting up ffmpeg 2 | 3 | You **must** have ffmpeg installed on your computer to enable the merging of the download chunks. 4 | 5 | ### Linux Users 6 | 7 | You can obtain the latest version from the repository for your distribution. 8 | 9 | ### macOS Users 10 | 11 | Download the prebuilt binaries and copy them to `/usr/local/bin` directory using the Terminal app. Be sure to also set their permissions 12 | using `chmod +x /usr/local/bin/ff*` 13 | 14 | ### Windows Users 15 | 16 | 1. Download the [ffmpeg binaries from here](http://ffmpeg.zeranoe.com/builds/). 17 | 2. Extract the .zip to any folder. 18 | 3. In LiveMe Tools, click `File` then `Preferences`. 19 | 4. Click on the `...` button for `FFMPEG` and browse into the newly created directory from the ffmpeg .zip. 20 | 5. Browse into the `bin` directory and select `ffmpeg.exe`. 21 | 6. Repeat steps 4 and 5 but for `FFPROBE`. 22 | 7. Click on `Test FFMPEG`, it should say that the test was successful. 23 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # LiveMe Tools 2 | 3 | *This project for the most part has been abandoned and the code is being left here for reference and use in future apps. A new fork/version of this is available at [https://github.com/polydragon/liveme-toolkit/](LiveMe Toolkit Repo).* 4 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # LiveMe Tools 2 | 3 | **This project has been replaced by the [LiveMe Pro Tools](https://github.com/thecoder75/liveme-pro-tools).** 4 | 5 | **The 6.x.x branch is considered the last stable branch.** 6 | **THe 7.x.x branch is considered to be in the beta stage.** 7 | 8 | ### Build Status 9 | **Windows:** [](https://ci.appveyor.com/project/thecoder75/liveme-tools/branch/master) **macOS/Linux:** [](https://travis-ci.org/thecoder75/liveme-tools) 10 | 11 | This is an Electron-based desktop app for Windows, macOS and Ubuntu Linux designed to: 12 | - Allow viewing a list of live videos with filter options 13 | - Search for users or videos tagged with hashtags 14 | - View details on users and their replays 15 | - Track previously viewed users 16 | - Watch and download replay videos 17 | - Create local Favorites lists without an account 18 | - Import and Export Favorites lists 19 | - Import a list of Replay URLs or VideoIDs for downloading 20 | - Ability to add a single URL 21 | - Uses a custom chunk downloader and FFMPEG to download replays 22 | - and much more! 23 | 24 | ## Getting Started 25 | 26 | ### Downloading and Installing 27 | 28 | [](https://github.com/thecoder75/liveme-tools/releases) 29 | 30 | *Click the button above to go to the downloads.* 31 | 32 | ### Building from Scratch 33 | 34 | You will need to download and install `yarn` package manager if you wish to build executables for Windows. This also relies on the [LiveMe API](https://github.com/thecoder75/liveme-api) module for the main communications with the Live.me servers. 35 | 36 | Extract to a folder and execute either `yarn install` or `npm install` to install all of the required modules. 37 | 38 | To execute in developer mode, run `yarn dev` or `npm run dev`. To build executables for your OS, run `yarn dist` or `npm run dist`. 39 | 40 | ## Built With 41 | * [Electron](http://electron.atom.io) 42 | * [NodeJS](http://nodejs.org) 43 | 44 | ## Contributors 45 | * [thecoder75](https://github.com/thecoder75) 46 | * [polydragon](https://github.com/polydragon) 47 | * [zp](https://github.com/zp) 48 | 49 | ## Bug Hunters and Beta Testers 50 | * [slyfox99](https://github.com/slyfox99) 51 | * [thegeezer9999](https://github.com/thegeezer9999) 52 | * [jaylittt](https://github.com/jaylittt) 53 | * [ushall](https://github.com/ushall) 54 | * [destruck51](https://github.com/destruck51) 55 | * [mmind99](https://github.com/mmind99) 56 | 57 | ## License 58 | This project is licensed under the GPL-3 License - see the [LICENSE](LICENSE) 59 | file for details 60 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .DS_Store 60 | .php 61 | 62 | dist/* 63 | 64 | -------------------------------------------------------------------------------- /src/appicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecoder75/liveme-tools/c7220600cb7a8d193bbfcebfb5138d7aa9891084/src/appicon.icns -------------------------------------------------------------------------------- /src/appicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecoder75/liveme-tools/c7220600cb7a8d193bbfcebfb5138d7aa9891084/src/appicon.ico -------------------------------------------------------------------------------- /src/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecoder75/liveme-tools/c7220600cb7a8d193bbfcebfb5138d7aa9891084/src/appicon.png -------------------------------------------------------------------------------- /src/build/appicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecoder75/liveme-tools/c7220600cb7a8d193bbfcebfb5138d7aa9891084/src/build/appicon.icns -------------------------------------------------------------------------------- /src/build/appicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecoder75/liveme-tools/c7220600cb7a8d193bbfcebfb5138d7aa9891084/src/build/appicon.ico -------------------------------------------------------------------------------- /src/build/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecoder75/liveme-tools/c7220600cb7a8d193bbfcebfb5138d7aa9891084/src/build/appicon.png -------------------------------------------------------------------------------- /src/custom_modules/DataManager/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | DataManager Module 4 | 5 | */ 6 | 7 | const low = require('lowdb'), 8 | FileSync = require('lowdb/adapters/FileSync'), 9 | fs = require('fs-extra'), 10 | path = require('path'), 11 | events = require('events'), 12 | { app } = require('electron'), 13 | LiveMe = require('liveme-api'); 14 | 15 | var index = 0, adapter, db; 16 | 17 | class DataManager { 18 | 19 | constructor() { 20 | this._favorites = []; 21 | this._visited = []; 22 | this.events = new (events.EventEmitter)(); 23 | 24 | fs.ensureDirSync(path.join(app.getPath('appData'), app.getName())); 25 | adapter = new FileSync(path.join(app.getPath('appData'), app.getName(), 'livemetools_db.json')); 26 | db = low(adapter); 27 | } 28 | 29 | ResetDB() { 30 | db.defaults({ 31 | favorites: [], 32 | visited: [], 33 | downloaded: [] 34 | }).write(); 35 | } 36 | 37 | commitDatabases() { 38 | db.write(); 39 | } 40 | 41 | 42 | 43 | /* 44 | Favorites 45 | */ 46 | addFavorite(e) { 47 | 48 | LiveMe.getUserInfo(e.uid) 49 | .then(user => { 50 | 51 | db.get('favorites').push({ 52 | id : user.user_info.uid, 53 | face: user.user_info.face, 54 | nickname: user.user_info.uname, 55 | sex: user.user_info.sex > -1 ? ( user.user_info.sex > 0 ? 'male' : 'female') : '', 56 | level: user.user_info.level, 57 | video_count: user.count_info.video_count, 58 | usign: user.user_info.usign, 59 | stars: user.user_info.stars 60 | }); 61 | 62 | var list = db.get('favorites').cloneDeep().value(); 63 | this.events.emit('refresh_favorites', list); 64 | 65 | }); 66 | } 67 | 68 | loadFavorites() { 69 | var list = db.get('favorites').cloneDeep().value(); 70 | this.events.emit('refresh_favorites', list); 71 | } 72 | 73 | removeFavorite(u) { 74 | db.get('favorites').remove({ id: u }).write(); 75 | this.loadFavorites(); 76 | } 77 | 78 | 79 | updateFavorites() { 80 | var count = db.get('favorites').size().value(); 81 | if (count == index) { 82 | index = 0; 83 | } 84 | 85 | index = 0; 86 | this._updateFavorites(); 87 | 88 | } 89 | _updateFavorites() { 90 | LiveMe.getUserInfo(db.get('favorites['+index+'].id').value()) 91 | .then(user => { 92 | 93 | db.get('favorites').find({ id: user.user_info.uid }) 94 | .assign({ face: user.user_info.face }) 95 | .assign({ nickname: user.user_info.nickname }) 96 | .assign({ usign: user.user_info.usign }) 97 | .assign({ level: user.user_info.level }) 98 | .assign({ stars: user.user_info.stars }) 99 | .assign({ sex: user.user_info.sex > -1 ? ( user.user_info.sex > 0 ? 'male' : 'female') : '' }) 100 | .assign({ video_count: user.count_info.video_count }); 101 | 102 | }); 103 | 104 | index++; 105 | 106 | var count = db.get('favorites').size().value(); 107 | if (count == index) { 108 | index = 0; 109 | var list = db.get('favorites').cloneDeep().value(); 110 | this.events.emit('refresh_favorites', list); 111 | } else { 112 | this._updateFavorites(); 113 | } 114 | 115 | } 116 | 117 | isInFavorites(e) { 118 | var t = db.get('favorites').find({ id: e }).value(); 119 | return (t == 'undefined' || typeof t == 'undefined' || t == null) ? false : true; 120 | } 121 | 122 | 123 | importFavorites(e) { 124 | fs.readFile(e, 'utf8', (err, data) => { 125 | if (err) { 126 | dialog.showErrorBox('Import Error', 'There was an error when attempting to import your favorites'); 127 | console.error(err); 128 | } else { 129 | for (let id of data.split("\n")) { 130 | id = id.trim(); 131 | 132 | if (id.startsWith('#') || id.length == 0) { 133 | continue; 134 | } 135 | 136 | if (this.isInFavorites(id)) { 137 | continue; 138 | } else { 139 | 140 | db.get('favorites').push({ 141 | id : id, 142 | face: '', 143 | nickname: '', 144 | sex: '', 145 | level: 0, 146 | video_count: 0, 147 | usign: '', 148 | stars: 0 149 | }); 150 | }; 151 | } 152 | this.updateFavorites(); 153 | } 154 | }); 155 | } 156 | 157 | exportFavorites(e) { 158 | var ids = "# Generated by LiveMe-Tools", list = db.get('favorites').cloneDeep().value(); 159 | 160 | for (let o of list) { 161 | ids += "\n" + o.id; 162 | } 163 | 164 | fs.writeFile(e, ids, 'utf8', function(err) { 165 | if (err) { 166 | dialog.showErrorBox('Export Error', 'There was an error when attempting to export your favorites'); 167 | console.error(err); 168 | } 169 | }); 170 | 171 | } 172 | 173 | 174 | 175 | /* 176 | Tracking of Visited UserIds 177 | */ 178 | addTrackedVisited(e) { 179 | var t = db.get('visited').find({ id: e.id }).value(); 180 | 181 | if (t == 'undefined' || typeof t == 'undefined' || t == null) { 182 | db.get('visited').push({ 183 | id: e.id, 184 | dt: e.dt 185 | }); 186 | } 187 | 188 | } 189 | dropTrackedVisited(e) { 190 | db.get('visited').remove({ id: e.id }).write(); 191 | } 192 | wasVisited(e) { 193 | var dt = Math.floor(new Date().getTime() / 1000), 194 | t = db.get('visited').find({ id: e }).value(); 195 | 196 | if (t == 'undefined' || typeof t == 'undefined' || t == null) return false; 197 | 198 | if ((dt - t.dt) > 0) { 199 | db.get('visited').remove({ id: e.id }); 200 | return false; 201 | } else { 202 | return true; 203 | } 204 | } 205 | 206 | 207 | 208 | /* 209 | Tracking of Downloaded Replays 210 | */ 211 | addDownloaded(e) { 212 | var t = db.get('downloaded').find({ id: e }).value(); 213 | if (t == 'undefined' || typeof t == 'undefined' || t == null) { 214 | db.get('downloaded').push({ 215 | id: e, 216 | dt: Math.floor(new Date().getTime() / 1000) 217 | }).write(); 218 | } 219 | } 220 | wasDownloaded(e) { 221 | var t = db.get('downloaded').find({ id: e }).value(); 222 | return (t == 'undefined' || typeof t == 'undefined' || t == null) ? false : true; 223 | } 224 | 225 | 226 | } 227 | 228 | exports.DataManager = DataManager; 229 | -------------------------------------------------------------------------------- /src/custom_modules/DownloadManager/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | const wget = require('wget-improved'); 12 | const pMap = require('p-map'); 13 | const pProgress = require('p-progress'); 14 | const axios = require('axios'); 15 | const path = require("path"); 16 | const fs = require("fs-extra"); 17 | const events = require("events"); 18 | const { app, ipcMain, dialog } = require('electron'); 19 | const { exec } = require('child_process'); 20 | function uuidv4() { 21 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 22 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 23 | return v.toString(16); 24 | }); 25 | } 26 | class DownloadManager { 27 | constructor() { 28 | this._queue = new Map(); 29 | this._history = []; 30 | this._paused = false; 31 | this._running = false; 32 | this._ffmpegPath = 'ffmpeg'; 33 | this.events = new (events.EventEmitter)(); 34 | } 35 | _emit(channel, obj) { 36 | if (this._eventCache && (this._eventCache.channel === channel && JSON.stringify(this._eventCache.obj) === JSON.stringify(obj))) { 37 | return; 38 | } 39 | else { 40 | this._eventCache = { 41 | channel: channel, 42 | obj: obj 43 | }; 44 | } 45 | this.events.emit(channel, obj); 46 | } 47 | _processChunk(url, dest) { 48 | return new pProgress((resolve, reject, progress) => { 49 | fs.ensureDirSync(path.dirname(dest)); 50 | let download = wget.download(url, dest); 51 | download.on('error', (err) => { 52 | return resolve({ success: false, error: err, local: dest }); 53 | }); 54 | download.on('start', (filesize) => { 55 | }); 56 | download.on('end', (output) => { 57 | return resolve({ success: true, local: dest }); 58 | }); 59 | download.on('progress', (p) => { 60 | progress({ chunk: path.basename(url, '.ts'), progress: p }); 61 | }); 62 | }); 63 | } 64 | _getTempFilename(url, uuid) { 65 | return path.join(this._appSettings.get('downloads.directory'), 'temp', uuid, path.basename(url)); 66 | } 67 | _getLocalFilename(playlist) { 68 | let defaultPath = path.join(this._appSettings.get('downloads.directory'), path.basename(playlist.video.url).replace("m3u8", "mp4")); 69 | let finalPath; 70 | if (this._appSettings.get('downloads.filemode') == 0) { 71 | finalPath = defaultPath; 72 | } 73 | else { 74 | let finalName = this._appSettings.get('downloads.filetemplate') 75 | .replace(/%%username%%/g, playlist.user.name) 76 | .replace(/%%userid%%/g, playlist.user.id) 77 | .replace(/%%videoid%%/g, playlist.video.id) 78 | .replace(/%%videotitle%%/g, playlist.video.title) 79 | .replace(/%%videotime%%/g, '' + playlist.video.time); 80 | if (!finalName || finalName == '') { 81 | finalPath = defaultPath; 82 | } 83 | else { 84 | finalPath = path.join(this._appSettings.get('downloads.directory'), finalName.replace(/[:*?""<>|]/g, '_') + ".mp4"); 85 | } 86 | } 87 | let basename = path.basename(finalPath); 88 | if (basename == 'playlist.mp4' || basename == 'playlist_eof.mp4') { 89 | let parentName = path.basename(path.dirname(playlist.video.url)); 90 | finalPath = finalPath.replace(basename, parentName + '.mp4'); 91 | } 92 | fs.ensureDirSync(path.dirname(finalPath)); 93 | return finalPath; 94 | } 95 | _getUrlsFromPlaylist(m3u8) { 96 | return new Promise((resolve, reject) => { 97 | return axios 98 | .get(m3u8) 99 | .then(response => { 100 | let playlist = [], baseURL = path.dirname(m3u8); 101 | for (let line of (response.data.split('\n'))) { 102 | line = line.trim(); 103 | if (line.length == 0 || line[0] == '#') { 104 | continue; 105 | } 106 | line = line.split('?').shift(); 107 | line = `${baseURL}/${line}`; 108 | if (playlist.indexOf(line) != -1) { 109 | continue; 110 | } 111 | playlist.push(line); 112 | } 113 | return resolve(playlist); 114 | }) 115 | .catch(err => { 116 | return reject(err); 117 | }); 118 | }); 119 | } 120 | _processItem(uuid, playlist) { 121 | return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { 122 | this._emit('download-started', { uuid: uuid }); 123 | let chunkProgress = new Map(); 124 | let downloadedChunks = 0; 125 | this._emit('download-status', { uuid: uuid, status: 'Getting chunks to download...' }); 126 | let urls = yield this._getUrlsFromPlaylist(playlist.video.url).catch(err => { 127 | return reject(`Couldn't get chunks to download: ${err}`); 128 | }); 129 | let mapper = el => this._processChunk(el, this._getTempFilename(el, uuid)).onProgress(p => { 130 | chunkProgress.set(p.chunk, p.progress); 131 | let total = 0; 132 | for (let val of chunkProgress.values()) { 133 | if (val) 134 | total += val; 135 | } 136 | total = Math.floor((total / urls.length) * 100); 137 | this._emit('download-progress', { uuid: uuid, percent: total }); 138 | }).then((result) => { 139 | downloadedChunks++; 140 | this._emit('download-status', { uuid: uuid, status: `Downloading chunks [${downloadedChunks}/${urls.length}]` }); 141 | return result; 142 | }); 143 | this._emit('download-status', { uuid: uuid, status: `Downloading chunks [0/${urls.length}]` }); 144 | pMap(urls, mapper, { concurrency: (this._appSettings.get('downloads.concurrency') || 4) }).then(result => { 145 | this._emit('download-status', { uuid: uuid, status: 'Merging chunks' }); 146 | if (result.length == 1) { 147 | let newPath = this._getLocalFilename(playlist); 148 | if (fs.existsSync(newPath)) { 149 | fs.removeSync(newPath); 150 | } 151 | fs.moveSync(result[0].local, newPath); 152 | return resolve(); 153 | } 154 | let concatStr = '# Generated by LiveMeTools'; 155 | let concatPath = this._getTempFilename('concat.txt', uuid); 156 | for (let res of result) { 157 | if (res.success) { 158 | concatStr += `\nfile '${res.local}'`; 159 | } 160 | else { 161 | return reject(`Failed to download at least one file`); 162 | } 163 | } 164 | fs.writeFileSync(concatPath, concatStr); 165 | exec(`${this._ffmpegPath} -y -f concat -safe 0 -i "${concatPath}" -c copy -bsf:a aac_adtstoasc -vsync 2 -movflags +faststart "${this._getLocalFilename(playlist)}"`, (error, stdout, stderr) => { 166 | if (error) { 167 | let log = path.join(this._appSettings.get('downloads.directory'), 'ffmpeg-error.log'); 168 | fs.writeFileSync(log, `${error}\n\n${stderr}\n\n${stdout}`); 169 | this._cleanupTempFiles(uuid); 170 | return reject(`FFMPEG Error, saved in: ${log}`); 171 | } 172 | this._cleanupTempFiles(uuid); 173 | return resolve(); 174 | }); 175 | }); 176 | })); 177 | } 178 | _cleanupTempFiles(uuid) { 179 | this._emit('download-status', { uuid: uuid, status: 'Cleaning temporary files' }); 180 | fs.removeSync(path.join(this._appSettings.get('downloads.directory'), 'temp', uuid)); 181 | } 182 | init(appSettings) { 183 | this._appSettings = appSettings; 184 | this._downloadQueuePath = path.join(app.getPath('appData'), app.getName(), 'download-queue-v2.json'); 185 | this._downloadHistoryPath = path.join(app.getPath('appData'), app.getName(), 'downloadHistory.json'); 186 | let mpeg = appSettings.get('downloads.ffmpeg'), probe = appSettings.get('downloads.ffprobe'); 187 | if (mpeg && mpeg != 'ffmpeg') { 188 | this._ffmpegPath = mpeg; 189 | } 190 | if (probe && probe != 'ffprobe') { 191 | } 192 | } 193 | add(playlist) { 194 | let uuid = uuidv4(); 195 | this._queue.set(uuid, playlist); 196 | this._emit('download-queued', { uuid: uuid, display: `${playlist.user.name}: ${playlist.video.id}` }); 197 | this.loop(); 198 | return uuid; 199 | } 200 | delete(uuid) { 201 | this._queue.delete(uuid); 202 | this._emit('download-deleted', { uuid: uuid }); 203 | } 204 | start(uuid) { 205 | let item = this._queue.get(uuid); 206 | this._queue.delete(uuid); 207 | return this._processItem(uuid, item) 208 | .then(result => { 209 | this._emit('download-completed', { uuid: uuid }); 210 | if (this._appSettings.get('downloads.history')) { 211 | this._history.push(item.video.id); 212 | } 213 | }) 214 | .catch(err => { 215 | this._emit('download-errored', { uuid: uuid, error: err }); 216 | }); 217 | } 218 | loop() { 219 | return __awaiter(this, void 0, void 0, function* () { 220 | if (this._running || this._paused) { 221 | return; 222 | } 223 | this._running = true; 224 | while (this._queue.size > 0 && !this._paused) { 225 | yield this.start(this._queue.keys().next().value); 226 | this.saveQueue(); 227 | } 228 | this._running = false; 229 | }); 230 | } 231 | isPaused() { 232 | return this._paused; 233 | } 234 | isRunning() { 235 | return this._running; 236 | } 237 | pause() { 238 | this._paused = true; 239 | this._emit('download-global-pause', null); 240 | } 241 | resume() { 242 | this._paused = false; 243 | this._emit('download-global-resume', null); 244 | this.loop(); 245 | } 246 | load() { 247 | this.loadQueue(); 248 | this.loadHistory(); 249 | } 250 | save() { 251 | this.saveQueue(); 252 | this.saveHistory(); 253 | } 254 | hasBeenDownloaded(videoid) { 255 | return this._history.indexOf(videoid) != -1; 256 | } 257 | purgeHistory() { 258 | fs.removeSync(this._downloadHistoryPath); 259 | this._history = []; 260 | } 261 | purgeQueue() { 262 | this._queue = new Map(); 263 | this.saveQueue(); 264 | this._emit('download-queue-clear', null); 265 | } 266 | setFfmpegPath(path) { 267 | this._ffmpegPath = path; 268 | } 269 | setFfprobePath(path) { 270 | } 271 | detectFFMPEG() { 272 | return new Promise((resolve, reject) => { 273 | exec(`${this._ffmpegPath} -codecs`, (error, stdout, stderr) => { 274 | if (error) { 275 | console.log('--------------------'); 276 | console.log('FFMPEG CHECK FAILED:'); 277 | console.log(error); 278 | console.log('--------------------'); 279 | return resolve(false); 280 | } 281 | return resolve(true); 282 | }); 283 | }); 284 | } 285 | saveQueue() { 286 | let spread = JSON.stringify([...this._queue]); 287 | fs.writeFile(this._downloadQueuePath, spread, 'utf8', (err) => { 288 | if (err) { 289 | console.error(err); 290 | } 291 | }); 292 | } 293 | saveHistory() { 294 | if (!this._appSettings.get('downloads.history')) { 295 | return; 296 | } 297 | fs.writeFile(this._downloadHistoryPath, JSON.stringify(this._history), 'utf8', (err) => { 298 | if (err) { 299 | console.log(err); 300 | } 301 | }); 302 | } 303 | loadQueue() { 304 | if (!fs.existsSync(this._downloadQueuePath)) { 305 | this._queue = new Map(); 306 | return; 307 | } 308 | fs.readFile(this._downloadQueuePath, 'utf8', (err, data) => { 309 | if (err) { 310 | console.error(err); 311 | } 312 | else { 313 | try { 314 | this._queue = new Map(JSON.parse(data)); 315 | } 316 | catch (err) { 317 | console.error(err); 318 | } 319 | } 320 | if (this._queue.size > 0) { 321 | for (let [key, playlist] of this._queue) { 322 | this._emit('download-queued', { uuid: key, display: `${playlist.user.name}: ${playlist.video.id}` }); 323 | } 324 | this.loop(); 325 | } 326 | }); 327 | } 328 | loadHistory() { 329 | if (!this._appSettings.get('downloads.history')) { 330 | return; 331 | } 332 | if (!fs.existsSync(this._downloadHistoryPath)) { 333 | this._history = []; 334 | return; 335 | } 336 | fs.readFile(this._downloadHistoryPath, 'utf8', (err, data) => { 337 | if (err) { 338 | console.log(err); 339 | this._history = []; 340 | } 341 | else { 342 | try { 343 | this._history = JSON.parse(data); 344 | } 345 | catch (err) { 346 | console.log(err); 347 | this._history = []; 348 | } 349 | } 350 | }); 351 | } 352 | } 353 | exports.DownloadManager = DownloadManager; 354 | -------------------------------------------------------------------------------- /src/custom_modules/Favorites/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Favorites Module 3 | */ 4 | "use strict"; 5 | 6 | const { app, dialog } = require('electron'), 7 | path = require('path'), axios = require('axios'), fs = require('fs'), 8 | eventEmitter = new(require('events').EventEmitter)(); 9 | 10 | var fav_list = [], last_change = 0, is_saved = false, index = 0, test_var = 0; 11 | 12 | 13 | module.exports = { 14 | 15 | events: eventEmitter, 16 | 17 | add : function(e) { 18 | fav_list.push(e); 19 | update_single_user(fav_list.length - 1); 20 | eventEmitter.emit('refresh', fav_list); 21 | }, 22 | 23 | refresh: function() { 24 | eventEmitter.emit('status', 'Loading list, please wait...'); 25 | eventEmitter.emit('refresh', fav_list); 26 | }, 27 | 28 | remove: function(e) { 29 | var idx = 0; 30 | for (var i = 0; i < fav_list.length; i++) { 31 | if (fav_list[i].uid == e) { 32 | fav_list.splice(i, 1); 33 | } 34 | } 35 | 36 | write_to_file(); 37 | eventEmitter.emit('removeSingle', { userid: e }); 38 | }, 39 | 40 | save: function() { 41 | write_to_file(); 42 | eventEmitter.emit('refresh', fav_list); 43 | }, 44 | 45 | load: function() { 46 | eventEmitter.emit('status', 'Loading list, please wait...'); 47 | fs.readFile(path.join(app.getPath('appData'), app.getName(), 'favorites.json'), 'utf8', function (err,data) { 48 | if (err) { 49 | fav_list = []; 50 | } else { 51 | var i, j = JSON.parse(data); 52 | for (i = 0; i < j.length; i++) { 53 | fav_list.push({ 54 | 'uid' : j[i].uid, 55 | 'face' : j[i].face, 56 | 'nickname' : j[i].nickname, 57 | 'sex' : j[i].sex, 58 | 'level' : j[i].level, 59 | 'video_count' : j[i].video_count, 60 | 'usign' : j[i].usign, 61 | 'stars' : j[i].stars 62 | }) 63 | } 64 | 65 | fav_list = JSON.parse(data); 66 | eventEmitter.emit('refresh', fav_list); 67 | } 68 | }); 69 | 70 | }, 71 | 72 | isOnList: function(e) { 73 | for (var i = 0; i < fav_list.length; i++) { 74 | if (fav_list[i].uid == e) return true; 75 | } 76 | return false; 77 | }, 78 | 79 | tick : function() { }, 80 | forceSave : function() { write_to_file(true); }, 81 | update: function() { 82 | eventEmitter.emit('status', 'Refreshing list, please wait...'); 83 | index = 0; 84 | update_favorites_list(); 85 | //eventEmitter.emit('refresh', fav_list); 86 | }, 87 | 88 | export: function(file) { 89 | let ids = "# Generated by LiveMe-Tools"; 90 | 91 | for (let o of fav_list) { 92 | ids += "\n" + o['uid']; // Just use \n even though windows uses \r\n. It will look stupid in Notepad, but it will still work if this file gets transferred between Windows and Linux/Mac. The importer will parse for \n, not \r\n. 93 | } 94 | 95 | fs.writeFile(file, ids, 'utf8', function(err) { 96 | if (err) { 97 | dialog.showErrorBox('Export Error', 'There was an error when attempting to export your favorites'); 98 | console.error(err); 99 | } 100 | }); 101 | }, 102 | 103 | import: function(file) { 104 | fs.readFile(file, 'utf8', (err, data) => { 105 | if (err) { 106 | dialog.showErrorBox('Import Error', 'There was an error when attempting to import your favorites'); 107 | console.error(err); 108 | } else { 109 | for (let id of data.split("\n")) { 110 | id = id.trim(); 111 | 112 | if (id.startsWith('#') || id.length == 0) { 113 | continue; 114 | } 115 | 116 | if (this.isOnList(id)) { 117 | continue; 118 | } 119 | 120 | fav_list.push({ uid: id }); 121 | } 122 | 123 | this.update(); 124 | } 125 | }); 126 | } 127 | } 128 | 129 | 130 | 131 | 132 | 133 | function write_to_file() { 134 | var ti = new Date().getTime() / 1000; 135 | last_change = ti; 136 | 137 | fs.writeFile(path.join(app.getPath('appData'), app.getName(), 'favorites.json'), JSON.stringify(fav_list, null, 2), function(){ }); 138 | } 139 | 140 | function read_from_file(cb) { 141 | fs.readFile(path.join(app.getPath('appData'), app.getName(), 'favorites.json'), 'utf8', function (err,data) { 142 | if (err) { 143 | fav_list = []; 144 | } else { 145 | fav_list = JSON.parse(data); 146 | last_change = new Date().getTime() / 1000; 147 | cb(fav_list); 148 | } 149 | }); 150 | 151 | } 152 | 153 | function update_single_user(index) { 154 | 155 | axios.get('http://live.ksmobile.net/user/getinfo',{ 156 | params: { 157 | userid: fav_list[index].uid 158 | } 159 | }).then(function(resp) { 160 | var j = resp.data.data.user; 161 | 162 | if (resp.data.status == 200) { 163 | fav_list[index].face = j.user_info.face; 164 | fav_list[index].nickname = j.user_info.nickname; 165 | fav_list[index].usign = j.user_info.usign; 166 | fav_list[index].level = j.user_info.level; 167 | fav_list[index].stars = j.user_info.star; 168 | fav_list[index].currency = j.user_info.currency; 169 | fav_list[index].stars = j.user_info.star; 170 | fav_list[index].stars = j.user_info.star; 171 | fav_list[index].stars = j.user_info.star; 172 | fav_list[index].sex = j.user_info.sex > -1 ? ( j.user_info.sex > 0 ? 'male' : 'female') : ''; 173 | fav_list[index].video_count = j.count_info.video_count; 174 | } 175 | 176 | fs.writeFile(path.join(app.getPath('appData'), app.getName(), 'favorites.json'), JSON.stringify(fav_list, null, 2), function(){ 177 | eventEmitter.emit('refresh', fav_list); 178 | }); 179 | 180 | }).catch(function(err){ 181 | console.log('Error during favorites list update: ' + err); 182 | }); 183 | } 184 | 185 | function update_favorites_list() { 186 | if (fav_list.length == 0) return; 187 | 188 | axios.get('http://live.ksmobile.net/user/getinfo',{ 189 | params: { 190 | userid: fav_list[index].uid 191 | } 192 | }).then(function(resp) { 193 | var j = resp.data.data.user; 194 | 195 | if (resp.data.status == 200) { 196 | fav_list[index].face = j.user_info.face; 197 | fav_list[index].nickname = j.user_info.nickname; 198 | fav_list[index].usign = j.user_info.usign; 199 | fav_list[index].level = j.user_info.level; 200 | fav_list[index].stars = j.user_info.star; 201 | fav_list[index].currency = j.user_info.currency; 202 | fav_list[index].stars = j.user_info.star; 203 | fav_list[index].stars = j.user_info.star; 204 | fav_list[index].stars = j.user_info.star; 205 | fav_list[index].sex = j.user_info.sex > -1 ? ( j.user_info.sex > 0 ? 'male' : 'female') : ''; 206 | fav_list[index].video_count = j.count_info.video_count; 207 | } 208 | index++; 209 | 210 | if (index == fav_list.length) { 211 | fs.writeFile(path.join(app.getPath('appData'), app.getName(), 'favorites.json'), JSON.stringify(fav_list, null, 2), function(){ 212 | eventEmitter.emit('refresh', fav_list); 213 | }); 214 | } 215 | 216 | eventEmitter.emit('status', 'Refreshing '+index+' of ' + fav_list.length + '...'); 217 | update_favorites_list(); 218 | 219 | }).catch(function(err){ 220 | console.log('Error during favorites list update: ' + err); 221 | }); 222 | } 223 | -------------------------------------------------------------------------------- /src/dist/empty.txt: -------------------------------------------------------------------------------- 1 | This is just an empty file. -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | _ _ __ __ _______ _ 4 | | | (_) | \/ | |__ __| | | 5 | | | ___ _____| \ / | ___ | | ___ ___ | |___ 6 | | | | \ \ / / _ \ |\/| |/ _ \ | |/ _ \ / _ \| / __| 7 | | |____| |\ V / __/ | | | __/ | | (_) | (_) | \__ \ 8 | |______|_| \_/ \___|_| |_|\___| |_|\___/ \___/|_|___/ 9 | 10 | 11 | Licensed under GPL3 now 12 | 13 | Developers: 14 | 15 | thecoder - https://github.com/thecoder75 16 | polydragon - https://github.com/polydragon 17 | 18 | */ 19 | const { app, BrowserWindow, ipcMain, Menu, shell, dialog } = require('electron'), 20 | os = require('os'), 21 | fs = require('fs'), 22 | isDev = require('electron-is-dev'), 23 | path = require('path'), 24 | request = require('request'), 25 | appSettings = require('electron-settings'), 26 | DataManager = new(require('./custom_modules/DataManager').DataManager)(), 27 | DownloadManager = new (require('./custom_modules/DownloadManager').DownloadManager)(), 28 | LiveMe = require('liveme-api'); 29 | 30 | let mainwin = null, 31 | queuewin = null, 32 | playerWindow = null, 33 | favoritesWindow = null, 34 | chatWindow = null, 35 | importwin = null, 36 | aboutwin = null, 37 | updatewin = null, 38 | livemeomg = null; 39 | 40 | function startApplication() { 41 | if (!appSettings.get('downloads.directory')) { 42 | appSettings.set('downloads', { 43 | directory: path.join(app.getPath('home'), 'Downloads'), 44 | filemode: 0, 45 | filetemplate: '', 46 | history: true, 47 | replaycount: 10, 48 | engine: 'internal' 49 | }); 50 | } 51 | 52 | var mainpos = { x: -1, y: -1 }, queuepos = { x: -1, y: -1 }, mainsize = [980, 540], queuesize = [640, 400]; 53 | 54 | if (!appSettings.get('windowpos.main')) { 55 | appSettings.set('windowpos', { 56 | main: JSON.stringify([ -1, -1]), 57 | queue: JSON.stringify([ -1, -1]), 58 | player: JSON.stringify([ -1, -1]), 59 | favorites: JSON.stringify([ -1, -1]), 60 | messages: JSON.stringify([ -1, -1]) 61 | }); 62 | appSettings.set('windowsize', { 63 | main: JSON.stringify([ 980, 560]), 64 | queue: JSON.stringify([ 640, 400]), 65 | player: JSON.stringify([ 368, process.platform == 'darwin' ? 640 : 664]), 66 | favorites: JSON.stringify([ 360, 720]), 67 | messages: JSON.stringify([ 360, 480]) 68 | }); 69 | } else { 70 | mainpos = JSON.parse(appSettings.get('windowpos.main')); 71 | queuepos = JSON.parse(appSettings.get('windowpos.queue')); 72 | mainsize = JSON.parse(appSettings.get('windowsize.main')); 73 | queuesize = JSON.parse(appSettings.get('windowsize.queue')); 74 | } 75 | 76 | 77 | mainwin = new BrowserWindow({ 78 | icon: __dirname + '/appicon.ico', 79 | x: mainpos[0] != -1 ? mainpos[0] : null, 80 | y: mainpos[1] != -1 ? mainpos[1] : null, 81 | width: mainsize[0], 82 | height: mainsize[1], 83 | minWidth: 980, 84 | minHeight: 560, 85 | darkTheme: true, 86 | autoHideMenuBar: false, 87 | disableAutoHideCursor: true, 88 | titleBarStyle: 'default', 89 | fullscreen: false, 90 | maximizable: true, 91 | closable: true, 92 | frame: true, 93 | backgroundColor: '#000000', 94 | show: false, 95 | webPreferences: { 96 | webSecurity: false, 97 | textAreasAreResizable: false, 98 | plugins: true 99 | } 100 | }); 101 | 102 | mainwin 103 | .on('ready-to-show', () => { 104 | // mainwin.show(); 105 | }); 106 | 107 | mainwin.on('close', () => { 108 | appSettings.set('windowpos.main', JSON.stringify(mainwin.getPosition()) ); 109 | appSettings.set('windowsize.main', JSON.stringify(mainwin.getSize()) ); 110 | }); 111 | mainwin.on('closed', () => { 112 | shutdownApp(); 113 | }); 114 | 115 | mainwin.loadURL(`file://${__dirname}/lmt/index.html`); 116 | 117 | queuewin = new BrowserWindow({ 118 | x: queuepos[0] != -1 ? queuepos[0] : null, 119 | y: queuepos[1] != -1 ? queuepos[1] : null, 120 | width: queuesize[0], 121 | height: queuesize[1], 122 | resizable: true, 123 | minWidth: 640, 124 | maxWidth: 640, 125 | minHeight: 200, 126 | maxHeight: 1600, 127 | darkTheme: true, 128 | autoHideMenuBar: true, 129 | show: false, 130 | skipTaskbar: false, 131 | disableAutoHideCursor: true, 132 | titleBarStyle: 'default', 133 | fullscreen: false, 134 | minimizable: true, 135 | maximizable: false, 136 | closable: false, 137 | frame: true, 138 | backgroundColor: '#000000', 139 | webPreferences: { 140 | webSecurity: false, 141 | plugins: true, 142 | devTools: true 143 | } 144 | }); 145 | 146 | queuewin.setMenu(null); 147 | queuewin 148 | .on('closed', () => { 149 | queuewin = null; 150 | }) 151 | .loadURL(`file://${__dirname}/lmt/queue.html`); 152 | queuewin.on('close', () => { 153 | appSettings.set('windowpos.queue', JSON.stringify(queuewin.getPosition()) ); 154 | appSettings.set('windowsize.queue', JSON.stringify(queuewin.getSize()) ); 155 | }); 156 | 157 | 158 | updatewin = new BrowserWindow({ 159 | width: 480, 160 | height: 80, 161 | resizable: false, 162 | darkTheme: true, 163 | autoHideMenuBar: true, 164 | show: false, 165 | skipTaskbar: true, 166 | disableAutoHideCursor: true, 167 | titleBarStyle: 'default', 168 | fullscreen: false, 169 | minimizable: false, 170 | maximizable: false, 171 | closable: true, 172 | frame: false, 173 | vibrancy: 'ultra-dark', 174 | backgroundColor: '#000000', 175 | webPreferences: { 176 | webSecurity: false, 177 | plugins: true, 178 | devTools: true 179 | } 180 | }); 181 | updatewin.setMenu(null); 182 | updatewin 183 | .on('closed', () => { 184 | updatewin = null; 185 | }) 186 | .loadURL(`file://${__dirname}/lmt/update.html`); 187 | 188 | // Build our custom menubar 189 | Menu.setApplicationMenu(Menu.buildFromTemplate(getMenuTemplate())); 190 | 191 | setInterval(function(){ 192 | // We want to commit the database every 300 seconds 193 | // to avoid too many system writes. 194 | DataManager.commitDatabases(); 195 | }, (300 * 1000)); 196 | 197 | setTimeout(function () { 198 | CheckForUpgrade(); 199 | }, 10000); 200 | 201 | setTimeout(function() { 202 | queuewin.minimize(); 203 | }, 50); 204 | 205 | setTimeout(() => { 206 | showSplash(); 207 | }, 500); 208 | 209 | //DataManager.load(); 210 | DownloadManager.init(appSettings); 211 | global.DataManager = DataManager; 212 | global.DownloadManager = DownloadManager; 213 | 214 | DownloadManager.events.on('show-queue', () => { 215 | if (queuewin) { 216 | queuewin.showInactive(); 217 | } 218 | }); 219 | 220 | setTimeout(function() { 221 | if (!fs.existsSync(path.join(app.getPath('appData'), app.getName(), 'favorites.json'))) { 222 | aboutwin.on('close', () => { 223 | mainwin.show(); 224 | }); 225 | } else { 226 | updatewin.on('close', () => { 227 | mainwin.show(); 228 | }); 229 | 230 | aboutwin.on('close', () => { 231 | updatewin.show(); 232 | }); 233 | } 234 | }, 500); 235 | 236 | 237 | } 238 | 239 | var shouldQuit = app.makeSingleInstance(function (commandLine, workingDirectory) { 240 | }); 241 | 242 | if (shouldQuit) { 243 | shutdownApp(); 244 | return; 245 | } 246 | 247 | app 248 | .on('ready', startApplication) 249 | .on('activate', () => { 250 | if (mainwin === null) { 251 | startApplication(); 252 | } 253 | }); 254 | 255 | 256 | 257 | /* 258 | Splash/About Window 259 | */ 260 | function showSplash() { 261 | aboutwin = new BrowserWindow({ 262 | width: 640, 263 | height: 224, 264 | resizable: false, 265 | darkTheme: true, 266 | autoHideMenuBar: true, 267 | show: false, 268 | skipTaskbar: true, 269 | disableAutoHideCursor: true, 270 | titleBarStyle: 'default', 271 | fullscreen: false, 272 | backgroundColor: '#000000', 273 | maximizable: false, 274 | frame: false, 275 | movable: false, 276 | transparent: true, 277 | parent: mainwin 278 | }); 279 | aboutwin.setMenu(null); 280 | aboutwin 281 | .on('ready-to-show', () => { 282 | aboutwin.show(); 283 | }) 284 | .loadURL(`file://${__dirname}/lmt/splash.html`); 285 | 286 | } 287 | 288 | function showSettings() { 289 | let settingsWindow = new BrowserWindow({ 290 | width: 900, 291 | height: 420, 292 | resizable: false, 293 | darkTheme: true, 294 | autoHideMenuBar: true, 295 | show: false, 296 | skipTaskbar: false, 297 | center: true, 298 | backgroundColor: '#000000', 299 | disableAutoHideCursor: true, 300 | titleBarStyle: 'default', 301 | fullscreen: false, 302 | maximizable: false, 303 | closable: true, 304 | frame: true, 305 | parent: mainwin, 306 | modal: false 307 | }); 308 | 309 | settingsWindow.setMenu(null); 310 | 311 | settingsWindow 312 | .on('ready-to-show', () => { 313 | settingsWindow.show(); 314 | }) 315 | .loadURL(`file://${__dirname}/lmt/settings.html`); 316 | }; 317 | 318 | 319 | function showLiveMeOMG() { 320 | let livemeomg = new BrowserWindow({ 321 | width: 640, 322 | height: 540, 323 | resizable: true, 324 | darkTheme: true, 325 | autoHideMenuBar: true, 326 | show: false, 327 | skipTaskbar: false, 328 | center: true, 329 | backgroundColor: '#000000', 330 | disableAutoHideCursor: true, 331 | titleBarStyle: 'default', 332 | fullscreen: false, 333 | maximizable: true, 334 | closable: true, 335 | frame: true, 336 | modal: false 337 | }); 338 | 339 | livemeomg.setMenu(null); 340 | 341 | livemeomg 342 | .on('ready-to-show', () => { 343 | livemeomg.show(); 344 | }) 345 | .loadURL(`file://${__dirname}/lmt/livemeomg.html`); 346 | }; 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | /* 359 | Favorites Related 360 | */ 361 | function openFavoritesWindow() { 362 | if (favoritesWindow == null) { 363 | 364 | var favpos = JSON.parse(appSettings.get('windowpos.favorites')), 365 | favsize = JSON.parse(appSettings.get('windowsize.favorites')); 366 | 367 | favoritesWindow = new BrowserWindow({ 368 | x: favpos[0] != -1 ? favpos[0] : null, 369 | y: favpos[1] != -1 ? favpos[1] : null, 370 | width: favsize[0], 371 | height: favsize[1], 372 | resizable: false, 373 | darkTheme: true, 374 | autoHideMenuBar: true, 375 | show: false, 376 | skipTaskbar: false, 377 | backgroundColor: '#000000', 378 | disableAutoHideCursor: true, 379 | titleBarStyle: 'default', 380 | fullscreen: false, 381 | maximizable: false, 382 | closable: true, 383 | frame: true, 384 | webPreferences: { 385 | webSecurity: false, 386 | plugins: true, 387 | devTools: true 388 | } 389 | }); 390 | favoritesWindow.setMenu(null); 391 | favoritesWindow 392 | .on('ready-to-show', () => { 393 | favoritesWindow.show(); 394 | }) 395 | .on('closed', () => { 396 | favoritesWindow = null; 397 | }) 398 | .on('close', () => { 399 | appSettings.set('windowpos.favorites', JSON.stringify(favoritesWindow.getPosition()) ); 400 | appSettings.set('windowsize.favorites', JSON.stringify(favoritesWindow.getSize()) ); 401 | }) 402 | .loadURL(`file://${__dirname}/lmt/favorites-list.html`); 403 | 404 | } 405 | }; 406 | 407 | ipcMain.on('show-settings', (event, arg) => { 408 | showSettings(); 409 | }); 410 | 411 | 412 | 413 | /* 414 | Search Related 415 | ?? - Called from Following/Fans/Favorites windows when an entry is clicked. Will 416 | need to clean up and standardize as a single command 417 | */ 418 | 419 | ipcMain.on('submit-search', (event, arg) => { 420 | mainwin.webContents.send('do-search', { userid: arg.userid }); 421 | }); 422 | 423 | ipcMain.on('livemesearch', (event, arg) => { 424 | if (arg.type == 'search') { 425 | lmt.searchkeyword(arg.query, function (e) { 426 | mainwin.webContents.send('render_results', { data: e, type: 'search' }); 427 | }) 428 | } else { 429 | lmt.getuservideos(arg.query, function (e) { 430 | mainwin.webContents.send('render_results', { data: e, type: 'userlookup' }); 431 | }) 432 | } 433 | }); 434 | 435 | 436 | 437 | /* 438 | Popup Windows (Followings/Fans) 439 | */ 440 | ipcMain.on('open-window', (event, arg) => { 441 | 442 | let win = new BrowserWindow({ 443 | width: 320, 444 | height: 720, 445 | resizable: false, 446 | darkTheme: true, 447 | autoHideMenuBar: false, 448 | skipTaskbar: false, 449 | backgroundColor: '#000000', 450 | disableAutoHideCursor: true, 451 | titleBarStyle: 'default', 452 | fullscreen: false, 453 | maximizable: false, 454 | closable: true, 455 | frame: true, 456 | show: false 457 | }); 458 | win.setMenu(null); 459 | 460 | win.on('ready-to-show', () => { 461 | win.show(); 462 | }).loadURL(`file://${__dirname}/lmt/` + arg.url); 463 | }); 464 | 465 | 466 | 467 | /* 468 | Video Player Related 469 | 470 | macOS allows us to still have window controls but no titlebar 471 | and overlay the controls on the content of the page. 472 | 473 | This allows us to have a window just like QuickTime Player 474 | does. 475 | */ 476 | ipcMain.on('play-video', (event, arg) => { 477 | if (playerWindow == null) { 478 | 479 | var playerpos = JSON.parse(appSettings.get('windowpos.player')), 480 | playersize = JSON.parse(appSettings.get('windowsize.player')); 481 | 482 | playerWindow = new BrowserWindow({ 483 | x: playerpos[0] != -1 ? playerpos[0] : null, 484 | y: playerpos[1] != -1 ? playerpos[1] : null, 485 | width: playersize[0], 486 | height: playersize[1], 487 | minWidth: 368, 488 | minHeight: process.platform == 'darwin' ? 640 : 664, 489 | resizable: true, 490 | darkTheme: true, 491 | autoHideMenuBar: false, 492 | show: false, 493 | skipTaskbar: false, 494 | backgroundColor: '#000000', 495 | disableAutoHideCursor: true, 496 | titleBarStyle: 'hidden', 497 | fullscreen: false, 498 | maximizable: true, 499 | closable: true, 500 | frame: process.platform == 'darwin' ? false : true 501 | }); 502 | playerWindow.setMenu(null); 503 | 504 | /* 505 | if (process.platform == 'darwin') { 506 | playerWindow.setAspectRatio(9 / 16); 507 | } 508 | */ 509 | 510 | playerWindow 511 | .on('ready-to-show', () => { 512 | playerWindow.show(); 513 | }) 514 | .on('close', () => { 515 | appSettings.set('windowpos.player', JSON.stringify(playerWindow.getPosition()) ); 516 | appSettings.set('windowsize.player', JSON.stringify(playerWindow.getSize()) ); 517 | }) 518 | .on('closed', () => { 519 | playerWindow = null; 520 | }); 521 | } 522 | 523 | playerWindow.loadURL(`file://${__dirname}/lmt/player.html#` + arg.url); 524 | }); 525 | ipcMain.on('video-set-time', (event, arg) => { 526 | playerWindow.webContents.send('jump-to-time', { time: arg.time, label: arg.label }); 527 | }); 528 | 529 | /* 530 | Chat Window 531 | */ 532 | ipcMain.on('open-chat', (event, arg) => { 533 | 534 | var msgpos = JSON.parse(appSettings.get('windowpos.messages')), 535 | msgsize = JSON.parse(appSettings.get('windowsize.messages')); 536 | 537 | 538 | chatWindow = new BrowserWindow({ 539 | x: msgpos[0] != -1 ? msgpos[0] : null, 540 | y: msgpos[1] != -1 ? msgpos[1] : null, 541 | width: msgsize[0], 542 | height: msgsize[1], 543 | width: 360, 544 | height: 480, 545 | minWidth: 360, 546 | maxWidth: 360, 547 | minHeight: 240, 548 | maxHeight: 1600, 549 | resizable: true, 550 | darkTheme: true, 551 | autoHideMenuBar: false, 552 | show: false, 553 | skipTaskbar: false, 554 | backgroundColor: '#000000', 555 | disableAutoHideCursor: true, 556 | titleBarStyle: 'default', 557 | fullscreen: false, 558 | minimizable: false, 559 | maximizable: false, 560 | closable: true, 561 | frame: true 562 | }); 563 | chatWindow.setMenu(null); 564 | 565 | chatWindow 566 | .on('close', () => { 567 | appSettings.set('windowpos.messages', JSON.stringify(chatWindow.getPosition()) ); 568 | appSettings.set('windowsize.messages', JSON.stringify(chatWindow.getSize()) ); 569 | }) 570 | .on('closed', () => { 571 | chatWindow = null; 572 | }) 573 | .loadURL(`file://${__dirname}/lmt/chat.html?${arg.videoid}`); 574 | 575 | chatWindow.showInactive(); 576 | }); 577 | 578 | 579 | 580 | /* 581 | 582 | Queue Window 583 | 584 | show-queue is issued when only the first download is added to the queue 585 | */ 586 | ipcMain.on('show-queue', () => { 587 | if (queuewin.isMinimized()) { 588 | queuewin.restore(); 589 | } 590 | }); 591 | function toggleQueueWindow() { 592 | if (queuewin.isMinimized()) { 593 | queuewin.restore(); 594 | } else { 595 | queuewin.minimize(); 596 | } 597 | }; 598 | 599 | 600 | 601 | /* 602 | History Window 603 | 604 | */ 605 | 606 | ipcMain.on('history-delete', (event, arg) => { 607 | mainwin.send('history-delete', {}); 608 | }); 609 | 610 | 611 | 612 | 613 | /* 614 | 615 | Misc. Functions 616 | 617 | Here are the Import and Export functions and other functions we 618 | use. 619 | 620 | */ 621 | function importUrlList() { 622 | var d = dialog.showOpenDialog( 623 | { 624 | properties: [ 625 | 'openFile', 626 | ], 627 | buttonLabel : 'Import', 628 | filters : [ 629 | { name : 'Plain Text File', extensions: [ 'txt' ]} 630 | ] 631 | }, 632 | (filePaths) => { 633 | if (filePaths == null) return; 634 | // We have a selection... 635 | mainwin.send('show-status', { message : 'Importing file, please wait...' }); 636 | fs.readFile(filePaths[0], 'utf8', function (err,data) { 637 | if (err) { 638 | mainwin.send('update-status', { message : 'Import error while accessing the file.' }); 639 | setTimeout(function(){ 640 | mainwin.send('hide-status'); 641 | }, 2000); 642 | } else { 643 | var filelist = data.split('\n'); 644 | 645 | for (i = 0; i < filelist.length; i++) { 646 | if (filelist[i].indexOf('http') > -1) { 647 | Downloads.add({ 648 | user: { 649 | id: null, 650 | name: null 651 | }, 652 | video: { 653 | id: null, 654 | title: null, 655 | time: 0, 656 | url: filelist[i].trim() 657 | } 658 | }); 659 | 660 | } 661 | } 662 | } 663 | }); 664 | } 665 | );} 666 | 667 | function importVideoIdList() { 668 | var d = dialog.showOpenDialog( 669 | { 670 | properties: [ 671 | 'openFile', 672 | ], 673 | buttonLabel : 'Import', 674 | filters : [ 675 | { name : 'Plain Text File', extensions: [ 'txt' ]} 676 | ] 677 | }, 678 | (filePaths) => { 679 | // We have a selection... 680 | if (filePaths == null) return; 681 | mainwin.send('show-status', { message : 'Importing file, please wait...' }); 682 | 683 | fs.readFile(filePaths[0], 'utf8', function (err,data) { 684 | if (err) { 685 | mainwin.send('update-status', { message : 'Import error while accessing the file.' }); 686 | setTimeout(function(){ 687 | mainwin.send('hide-status'); 688 | }, 2500); 689 | } else { 690 | var tt = data.replace('\r', ''); 691 | var t = tt.split('\n'), i = 0, idlist = []; 692 | 693 | for (i = 0; i < t.length; i++) { 694 | if (t[i].length > 16) 695 | idlist.push(t[i].trim()); 696 | } 697 | 698 | mainwin.send('update-status', { message : 'Found '+idlist.length+' entries to import.' }); 699 | _importVideoIdList(idlist); 700 | 701 | setTimeout(function(){ 702 | mainwin.send('hide-status'); 703 | }, 2500); 704 | } 705 | }); 706 | } 707 | ); 708 | } 709 | 710 | function _importVideoIdList(list) { 711 | var entry = list.shift(); 712 | LiveMe.getVideoInfo(entry) 713 | .then(video => { 714 | 715 | if (video.vid.length > 16) { 716 | DownloadManager.add({ 717 | user: { 718 | id: video.userid, 719 | name: video.uname 720 | }, 721 | video: { 722 | id: video.vid, 723 | title: video.title, 724 | time: video.vtime, 725 | url: video.hlsvideosource 726 | } 727 | }); 728 | } 729 | 730 | if (list.length > 0) 731 | _importVideoIdList(list); 732 | else { 733 | mainwin.send('update-status', { message : 'Import complete.' }); 734 | setTimeout(function(){ 735 | mainwin.send('hide-status'); 736 | }, 1000); 737 | } 738 | }); 739 | } 740 | 741 | function exportFavorites() { 742 | let d = dialog.showSaveDialog( 743 | { 744 | filters: [ { name: "Text File", extensions: ["txt"] }, { name: 'All Files', extensions: ['*'] } ], 745 | defaultPath: 'Exported Favorites UserID List.txt' 746 | }, 747 | (filePath) => { 748 | 749 | if (filePath != null) 750 | DataManager.exportFavorites(filePath); 751 | } 752 | ); 753 | } 754 | 755 | function importFavorites() { 756 | let d = dialog.showOpenDialog( 757 | { 758 | filters: [ { name: "Text File", extensions: ["txt"] }, { name: 'All Files', extensions: ['*'] } ], 759 | defaultPath: 'Exported Favorites UserID List.txt' 760 | }, 761 | (filePath) => { 762 | if (filePath != null) 763 | DataManager.importFavorites(filePath[0]); 764 | } 765 | ); 766 | } 767 | 768 | function shutdownApp() { 769 | if (queuewin != null) { 770 | queuewin.setClosable(true); 771 | queuewin.close(); 772 | } 773 | 774 | if (updatewin != null) { 775 | updatewin.setClosable(true); 776 | updatewin.close(); 777 | } 778 | 779 | DataManager.commitDatabases(); 780 | DownloadManager.save(); 781 | //Downloader.killActiveDownload(); 782 | 783 | setTimeout(function(){ 784 | app.quit(); 785 | }, 500); 786 | } 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | /* 802 | 803 | Main Window/macOS Menubar menu template 804 | 805 | */ 806 | function getMenuTemplate() { 807 | var template = [ 808 | { 809 | label: 'Edit', 810 | submenu: [ 811 | { role: 'undo' }, 812 | { role: 'redo' }, 813 | { type: 'separator' }, 814 | { role: 'cut' }, 815 | { role: 'copy' }, 816 | { role: 'paste' }, 817 | { role: 'delete' }, 818 | { role: 'selectall' } 819 | ] 820 | }, 821 | { 822 | label : 'Favorites', 823 | submenu : [ 824 | { 825 | label: 'Open Favorites', 826 | accelerator: 'CommandOrControl+D', 827 | click: () => openFavoritesWindow() 828 | }, 829 | { 830 | type: 'separator' 831 | }, 832 | { 833 | label: 'Refresh Entries', 834 | click: () => DataManager.updateFavorites() 835 | }, 836 | { 837 | label: 'Export Favorites List', 838 | click: () => exportFavorites() 839 | }, 840 | { 841 | label: 'Import Favorites List', 842 | click: () => importFavorites() 843 | } 844 | ] 845 | }, 846 | { 847 | role: 'window', 848 | submenu: [ 849 | { role: 'minimize' }, 850 | { role: 'close' }, 851 | { type: 'separator' }, 852 | { 853 | label: 'Developer Tools', 854 | submenu: [ 855 | { role: 'reload' }, 856 | { role: 'forcereload' }, 857 | { role: 'toggledevtools' } 858 | ] 859 | } 860 | ] 861 | }, 862 | { 863 | role: 'help', 864 | submenu: [ 865 | { 866 | label: 'LiveMe Tools Page', 867 | click: () => shell.openExternal('https://thecoder75.github.io/liveme-tools/') 868 | }, 869 | { 870 | label: 'Help with FFMPEG Installation', 871 | click: () => shell.openExternal('https://github.com/thecoder75/liveme-tools/blob/master/docs/ffmpeg.md') 872 | }, 873 | { 874 | label: 'Report an Issue', 875 | click: () => shell.openExternal('https://github.com/thecoder75/liveme-tools/issues') 876 | } 877 | ] 878 | } 879 | ]; 880 | 881 | if (process.platform === 'darwin') { 882 | template.unshift({ 883 | label: 'File', 884 | submenu: [ 885 | { 886 | label: 'Import', 887 | submenu : [ 888 | { 889 | label: 'Import URL List', 890 | accelerator: 'CommandOrControl+I', 891 | click: () => importUrlList() 892 | }, 893 | { 894 | label: 'Import VideoID List', 895 | accelerator: 'CommandOrControl+Shift+I', 896 | click: () => importVideoIdList() 897 | } 898 | ] 899 | }, 900 | { type: 'separator' }, 901 | { 902 | label: 'Show LiveMe-OMG', 903 | click: () => showLiveMeOMG() 904 | } 905 | ] 906 | }); 907 | template.unshift({ 908 | label: app.getName(), 909 | submenu: [ 910 | { 911 | label: 'About ' + app.getName(), 912 | click: () => showSplash() 913 | }, 914 | { type: 'separator' }, 915 | { 916 | label : 'Preferences', 917 | accelerator: 'CommandOrControl+,', 918 | click: () => showSettings() 919 | }, 920 | { type: 'separator' }, 921 | { role: 'services', submenu: [] }, 922 | { type: 'separator' }, 923 | { role: 'hide' }, 924 | { role: 'hideothers' }, 925 | { role: 'unhide' }, 926 | { type: 'separator' }, 927 | { 928 | label: 'Quit ' + app.getName(), 929 | accelerator: 'CommandOrControl+Q', 930 | click: () => shutdownApp() 931 | } 932 | ] 933 | }); 934 | } else { 935 | template.unshift({ 936 | label: 'File', 937 | submenu: [ 938 | { 939 | label: 'Import', 940 | submenu : [ 941 | { 942 | label: 'Import URL List', 943 | accelerator: 'CommandOrControl+I', 944 | click: () => importUrlList() 945 | }, 946 | { 947 | label: 'Import VideoID List', 948 | accelerator: 'CommandOrControl+Shift+I', 949 | click: () => importVideoIdList() 950 | } 951 | ] 952 | }, 953 | { type: 'separator' }, 954 | { 955 | label: 'Show LiveMe-OMG', 956 | click: () => showLiveMeOMG() 957 | }, 958 | { type: 'separator' }, 959 | { 960 | label : 'Preferences', 961 | click: () => showSettings() 962 | }, 963 | { type: 'separator' }, 964 | { 965 | label: 'Quit', 966 | accelerator: 'CommandOrControl+F4', 967 | click: () => shutdownApp() 968 | } 969 | ] 970 | }); 971 | } 972 | 973 | return template; 974 | } 975 | 976 | 977 | function CheckForUpgrade() { 978 | var r = new Date().getTime(); 979 | 980 | request({ url: 'https://raw.githubusercontent.com/thecoder75/liveme-tools/master/src/package.json?random=' + r, timeout: 15000 }, function (err, response, body) { 981 | var js = JSON.parse(body), nv = parseFloat(js.version.replace('.', '')), ov = parseFloat(app.getVersion().replace('.', '')), isCurrent = nv > ov; 982 | 983 | if (nv > ov) { 984 | let win = new BrowserWindow({ 985 | width: 480, 986 | height: 280, 987 | resizable: false, 988 | darkTheme: true, 989 | autoHideMenuBar: false, 990 | skipTaskbar: false, 991 | vibrancy: 'ultra-dark', 992 | backgroundColor: process.platform == 'darwin' ? null : '#000000', // We utilize the macOS Vibrancy mode 993 | disableAutoHideCursor: true, 994 | titleBarStyle: 'default', 995 | fullscreen: false, 996 | maximizable: false, 997 | closable: true, 998 | frame: true, 999 | show: false 1000 | }); 1001 | 1002 | win.on('ready-to-show', () => { 1003 | win.show(); 1004 | }).loadURL(`file://${__dirname}/lmt/upgrade.html`); 1005 | } 1006 | }); 1007 | } 1008 | -------------------------------------------------------------------------------- /src/lmt/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecoder75/liveme-tools/c7220600cb7a8d193bbfcebfb5138d7aa9891084/src/lmt/appicon.png -------------------------------------------------------------------------------- /src/lmt/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
50 |
51 | Downloads52 | 53 |
54 | Folder to store downloads in:
55 |
60 |
61 |
59 |
62 |
79 |
80 |
63 |
75 |
64 |
68 | 65 | Enable Download History 66 | 67 |
69 |
70 |
71 |
72 |
73 |
74 | 76 | When enabled, highlights any videos if you previously downloaded them. 77 | 78 |
81 |
96 |
97 |
82 |
91 |
95 |
83 |
87 | 84 | FFMPEG Path 85 | 86 |
88 |
90 | Required for downloads 89 |
98 |
113 |
99 |
108 |
112 |
100 |
104 | 101 | FFPROBE Path 102 | 103 |
105 |
107 | Required for progress % 106 | |
114 |
115 |
116 |
117 | Viewing History118 |
119 | Keep visited profiles marked for:
120 |
130 |
131 |
121 |
128 |
129 | Filenames132 |
133 |
145 |
134 |
138 | 135 | Use Custom Filenames 136 | 137 |
139 |
140 |
141 |
142 |
143 |
144 |
146 | Filenames will be the playlist name in the URL.
147 |
148 |
149 | Filenames will be created using the template below:
150 |
151 |
162 |
163 |
164 |
159 | The filename of the playlist will be used if the custom title ends up being empty. 160 | 161 | |
165 |
38 | Looks like version has been released!
39 | You're running version .
40 |