bilibili history manage B站历史记录持久化并扩展搜索功能

B站历史记录持久化存储到浏览器的indexDB数据库中,提供更丰富的搜索功能

// ==UserScript==
// @name         bilibili history manage B站历史记录持久化并扩展搜索功能
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  B站历史记录持久化存储到浏览器的indexDB数据库中,提供更丰富的搜索功能
// @author       You
// @match        https://www.bilibili.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @require      http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.0.js
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @license      MIT
// @run-at       document-start

// ==/UserScript==
/*导入zl-indexdb开源库,并遵守MIT开源协议进行少量修改*/
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global['zl-indexdb'] = factory());
}(this, (function () { 'use strict';

  class IndexDBOperation {

      /**
       * @method getDB
       * @for IndexDBOperation
       * @description 得到IndexDBOperation实例,创建/打开/升级数据库
       * @param  {String} dbName 要创建或打开或升级的数据库名
       * @param  {Number} dbVersion 要创建或打开或升级的数据库版本
       * @param  {Array} objectStores 此数据库的对象仓库(表)信息集合
       * @param  {String} objectStores.objectStoreName  表名
       * @param  {Number} objectStores.type  表示创建数据库或者升级数据库时,这个数据仓库(表)的处理类型( 0:表示把已存在的删除,然后重新创建。 1:表示如果不存在才重新创建,2:表示只删除,不再进行重新创建)
       * @param  {Object}  objectStores.keyMode  这个表的key的模式,具有两个可选属性:keyPath,autoIncrement,如果都不写那么添加数据时就需要自动手动的指定key
       * @param  {String} objectStores.keyMode.keyPath   表示在添加数据时,从对象类型的值中取一个字段,作为这条记录的键值,如果作为值的对象中不含有此字段,那么就会报错。
       * @param  {Boolean} objectStores.keyMode.autoIncrement   表示自动生成的递增数字作为记录的键值,一般从1开始。
       * @param  {Array} objectStores.indexs  索引数组,每个对象元素都代表了一条设置的索引,{ indexName: "keyIndex", fieldName: "key", only: { unique: false } },//索引名,字段名,索引属性值是否唯一
       * @return  IDBRequest对象
       * @author zl-fire 2021/08/17
       * @example
       * // 初始化时,开始创建或者打开indexDB数据库,objectStores 会在升级函数里面执行
       * let objectStores = [
       * {
       *     objectStoreName: "notes",//表名
       *     type: 1,//0:表示把已存在的删除,然后重新创建。 1:表示如果不存在才重新创建,2:表示只删除,不再进行重新创建
       *     keyMode:  { autoIncrement: true, keyPath: 'recordID' },//key的模式: 【keyPath: 'recordID' 表示指定recordID为主键】, 【autoIncrement: true 表示设置主键key自动递增(没有指定具体主键的情况下,主键字段名默认为key)】,【两者可以同存,也可以只存在一个,甚至都不存在】
       *         { indexName: "keyIndex", fieldName: "key", only: { unique: false } },//索引名,字段名,索引属性值是否唯一
       *         { indexName: "emailIndex", fieldName: "email", only: { unique: false } },//索引名,字段名,索引属性值是否唯一
       *         { indexName: "doc_typeIndex", fieldName: "doc_type", only: { unique: false } },//索引名,字段名,索引属性值是否唯一
       *     ]
       * },
       * {
       *     objectStoreName: "users",//表名
       *     type: 1,//0:表示把已存在的删除,然后重新创建。 1:表示如果不存在才重新创建,2:表示只删除,不再进行重新创建
       *     keyMode: { keyPath: 'keyword' },//key的模式,使用时直接设置就可
       *     indexs: [
       *         { indexName: "keywordIndex", fieldName: "keyword", only: { unique: false } },//索引名,字段名,索引属性值是否唯一
       *     ]
       * }
       * ]
       * window.DB = new IndexDBOperation("taskNotes", 1, objectStores); //如果版本升级,请刷新下页面在执行升级操作
       */
      constructor(dbName, dbVersion, objectStores) {
          //先检查数据是否传入
          if (!(dbName && dbVersion && objectStores)) {
              console.error("dbName, dbVersion, objectStores参数都为必填项!"); return;
          }
          this.dbName = dbName;
          this.dbVersion = dbVersion;
          this.objectStores = objectStores;
          this.db = null;//数据库默认为null
          this.getDB();//得到数据库对象:this.db = 数据库对象;,这里可以不用await进行等待,因为后续会使用示例对象进行操作,并且还会判断db是否存在
      }

      /**
      * @method createObjectStores
      * @for IndexDBOperation
      * @description 创建对象仓库的函数
      * @param  {Object} db  indexDB数据库的对象
      * @param  {Object}  obj 此数据库的 对象仓库(表)相关信息,包含表名,keyMode,索引集合
      * @return {void}  无返回值
      * @author zl-fire 2021/08/17
      * @example
      * db: DB,
      * obj: {
      *     objectStoreName: "notes",//表名
      *     type: 1,//0:表示把已存在的删除,然后重新创建。 1:表示如果不存在才重新创建,2:表示只删除,不再进行重新创建
      *     keyMode:  { autoIncrement: true, keyPath: 'recordID' },//key的模式,使用时直接设置就可
      *     indexs: [
      *         { indexName: "keyIndex", fieldName: "key", only: { unique: false } },//索引名,字段名,索引属性值是否唯一
      *         { indexName: "emailIndex", fieldName: "email", only: { unique: false } },//索引名,字段名,索引属性值是否唯一
      *         { indexName: "doc_typeIndex", fieldName: "doc_type", only: { unique: false } },//索引名,字段名,索引属性值是否唯一
      *     ]
      * }
       */
      async createObjectStores(db, obj) {
          let store = db.createObjectStore(obj.objectStoreName, obj.keyMode);
          if (obj.indexs && obj.indexs instanceof Array) {
              obj.indexs.forEach(ele => {
                  store.createIndex(ele.indexName, ele.fieldName, ele.only); //索引名,字段名,索引属性值是否唯一
              });
          }
      }

      /**
       * 作用:创建/打开/升级数据库,
       * @method getDB
       * @for IndexDBOperation
       * @description 此方法自动将thid.db赋值为数据库对象
       * @return void
       */
      async getDB() {
          let { dbName, dbVersion, objectStores } = this;
          let _this = this;
          return new Promise(function (resolve, reject) {
              let request = window.indexedDB.open(dbName, dbVersion);
              request.onerror = function (event) {
                  console.log("数据库", dbName, "创建/打开失败!");
                  reject({
                      state: false,//失败标识
                      mes: "数据库" + dbName + "创建/打开失败!"
                  });
              };
              request.onsuccess = function (event) {
                  console.log("数据库", dbName, "创建/打开成功,拿到数据库对象了!");
                  _this.db = request.result;
                  resolve({
                      state: true,//成功标识
                  });
              };
              request.onupgradeneeded = function (event) {
                  console.log("数据库", dbName, "版本变化了!");
                  let db = event.target.result; //event中包含了数据库对象
                  //创建所有指定的表
                  objectStores.forEach(obj => {
                      switch (obj.type) {
                          case 0: //表示把已存在的删除,然后重新创建
                              if (db.objectStoreNames.contains(obj.objectStoreName)) {
                                  db.deleteObjectStore(obj.objectStoreName);
                              }
                              _this.createObjectStores(db, obj); //开始创建数据
                              break;
                          case 1: //表示如果不存在才重新创建
                              if (!db.objectStoreNames.contains(obj.objectStoreName)) {
                                  _this.createObjectStores(db, obj); //开始创建数据
                              }
                              break;
                          case 2: //表示只删除,不再进行重新创建
                              if (db.objectStoreNames.contains(obj.objectStoreName)) {
                                  db.deleteObjectStore(obj.objectStoreName);
                              }
                              break;
                      }
                  });

              };
          })
      }

      /**
       * 作用:给对象仓库(表)添加数据,可添加一条,也可批量添加多条,
       * @method addData
       * @for IndexDBOperation
       * @param  {Array} stores  数据要插入到那个对象仓库中去,如["hello"],类型为数组是为了后面好扩展新功能
       * @param  {Object|Array} data  要添加的数据,对象或对象数组
       * @return {boolean} true:添加成功,false:添加失败
       * @author zl-fire 2021/08/17
       * @example
       * //如果版本升级,请下刷新下页面在执行升级操作
       * let DB = new indexDBOperation("testDB4", 1, objectStores);
       *
       * //==============创建数据仓库时指定了keypath的模式==============
       *
       * //添加一条数据(添加的数据中包含了key,或者key会自动生成并自动递增)
       * let res = await DB.addData(["hello"], { name: "zs", age: 18 });
       *
       * //添加多条数据(添加的数据中包含了key,或者key会自动生成并自动递增)
       * let res2 = await DB.addData(["hello"], [
       * { name: "zss1", age: 18 },
       * { name: "zsd2", age: 18 },
       * { name: "zs3", age: 18 },
       * { name: "zsf4", age: 20 }
       * ]);
       *
       * //==============创建数据仓库时没有指定keypath的模式==============
       *
       * //添加一条数据(需要手动传入指定的key)
       * let res = await DB.addData(["hello"], { name: "zs", age: 18, thekeyName:"id" });
       *
       * //添加多条数据添加多条数据(需要手动传入指定的key:thekeyName)
       * let res2 = await DB.addData(["hello"], [
       * { name: "zss1", age: 18, thekeyName:"id1"  },
       * { name: "zsd2", age: 18, thekeyName:"id2" },
       * { name: "zs3", age: 18 , thekeyName:"id3" },
       * { name: "zsf4", age: 20 , thekeyName:"id4" }
       * ]);
       **/
      async addData(stores, data) {
          let _this = this;
          return new Promise(async function (resolve) {
              try {
                  //先检查数据是否传入
                  if (!(stores && data)) {
                      console.error("stores, data,参数都为必填项!"); return;
                  }
                  //  * 逻辑:先判断数据库是否已经被创建
                  if (_this.db === null) {
                      await _this.getDB();
                  }
                  //创建事物
                  let transaction = _this.db.transaction(stores, "readwrite"); //以可读写方式打开事物,该事务跨越stores中的表(object store)
                  let store = transaction.objectStore(stores[0]); //获取stores数组的第一个元素对象
                  //然后在判断要插入的数据类型:如果是数组,则循环添加插入
                  if (data instanceof Array) {
                      let resArr = [];
                      data.forEach(obj => {
                          let res;
                          if (obj.thekeyName) {
                              let key=obj.thekeyName;
                              delete obj.thekeyName;
                              res = store.add(obj, key);
                          }
                          else {
                              res = store.add(obj);
                          }
                          res.onsuccess = function (event) {
                              resArr.push('数据添加成功');
                              if (resArr.length == data.length) {
                                  resolve(true);
                              }
                          };
                          res.onerror = function (event) {
                              console.error(event);
                              resolve(false);
                          };
                      });

                  }
                  else {
                      let res;
                      if (data.thekeyName) {
                          // 表示用户在创建数据仓库时,没有设置keypath,所以添加数据时需要手动的指定
                          let key=data.thekeyName;
                          delete data.thekeyName;
                          res = store.add(data, key);
                      }
                      else {
                          res = store.add(data);
                      }
                      res.onsuccess = function (event) {
                          resolve(true);
                      };
                      res.onerror = function (event) {
                          resolve(false);
                          console.error(event);
                      };
                  }
              }
              catch (err) {
                  resolve(false);
                  console.error(err);
              }
          })
      }

      /**
       * @method queryBykeypath
       * @for IndexDBOperation
       * @description 通过keypath向对象仓库(表)查询数据,无参数则查询所有,
       * @param {Array}  stores  对象仓库数组,但是只取第一个对象仓库名
       * @param {String}  keypath  查询条件,keypath
       * @return {Object} 查询结果
       * @author zl-fire 2021/08/17
       * @example
       * //如果版本升级,请下刷新下页面在执行升级操作
       * let DB = new indexDBOperation("testDB4", 1, objectStores);
       *
       * //从hello表中查询主键(keypath)为5的单条数据
       * let res9 = await DB.queryBykeypath(['hello'],5)
       *
       * //从hello表中查询所有数据
       * let res10 = await DB.queryBykeypath(['hello'])
       **/
      async queryBykeypath(stores, keypath) {
          let _this = this;
          return new Promise(async function (resolve) {
              try {
                  //先检查数据是否传入
                  if (!stores) {
                      console.error("stores参数为必填项!"); return;
                  }
                  //  * 逻辑:先判断数据库是否已经被创建
                  if (_this.db === null) {
                      await _this.getDB();
                  }
                  var transaction = _this.db.transaction(stores, 'readwrite');
                  var store = transaction.objectStore(stores[0]);
                  if (keypath) {//查询单条数据
                      //从stores[0]表中通过keypath查询数据
                      var request = store.get(keypath);
                      request.onsuccess = function (e) {
                          var value = e.target.result;
                          resolve(value);
                      };
                  }
                  else {  //查询所有数据
                      let arr = [];
                      store.openCursor().onsuccess = function (event) {
                          var cursor = event.target.result; //游标:指向表第一条数据的指针
                          if (cursor) {
                              arr.push(cursor.value);
                              cursor.continue();
                          } else {
                              // console.log('没有更多数据了!');
                              resolve(arr);
                          }
                      };
                  }

              }
              catch (err) {
                  resolve(false);
                  console.error(err);
              }
          })
      }

      /**
       * 作用:通过索引index向对象仓库(表)查询数据,不传查询参数就查询所有数据
       * @method queryByIndex
       * @for IndexDBOperation
       * @param {Array} stores 对象仓库数组,但是只取第一个对象仓库名
       * @param {Object} indexObj 查询条件,索引对象,例:{name:"",value:"",uni:true},uni表示索引是否唯一,true唯一,false不唯一
       * @param {Object[]} where 在基于索引查出的数据中进一步筛选。对象数组 [{key,value,opt:"=="},{key,value,opt}] opt为操作符
       * @author zl-fire 2021/08/17
       * @example
       * //如果版本升级,请下刷新下页面在执行升级操作
       * let DB = new indexDBOperation("testDB4", 1, objectStores);
       *
       * //从hello表中查询nameIndex为zs3的单条数据
       * let res11 = await DB.queryByIndex(['hello'],{name:"nameIndex",value:"zs3",uni:true})
       *
       * //从hello表中查询ageIndex为18的多条数据
       * let res12 = await DB.queryByIndex(['hello'],{name:"ageIndex",value:"18"})
       *
       * //从hello表中查询ageIndex为18的多条数据,然后传入where进一步查询
       * let res12 = await DB.queryByIndex(['hello'],{name:"ageIndex",value:"18"},[{key:"sex",value:'男',opt:"=="}])
       **/
      async queryByIndex(stores, indexObj, where ,ps=1000) {
          let _this = this;
          return new Promise(async function (resolve) {
              try {
                  //先检查数据是否传入
                  if (!stores) {
                      console.error("stores参数为必填项!"); return;
                  }
                  //  * 逻辑:先判断数据库是否已经被创建
                  if (_this.db === null) {
                      await _this.getDB();
                  }
                  var transaction = _this.db.transaction(stores, 'readwrite');
                  var store = transaction.objectStore(stores[0]);
                  if (indexObj) {//按索引查询数据
                      // 如果索引时唯一的
                      if (indexObj.uni) {
                          var index = store.index(indexObj.name);
                          index.get(indexObj.value).onsuccess = function (e) {
                              var v = e.target.result;
                              resolve(v);
                          };
                      }
                      // 如果索引是不唯一的
                      else {
                          let arr = [];
                          var index = store.index(indexObj.name);
                          var request = index.openCursor(IDBKeyRange.upperBound(indexObj.value),'prev');/* 这里迎合需求改掉了*/
                          var count = 0
                          request.onsuccess = function (e) {
                              var cursor = e.target.result;
                              if (cursor&&count<ps) {
                                  var v = cursor.value;
                                  arr.push(v);
                                  cursor.continue();
                                  count++
                              }
                              else {
                                  // 如果还需要进一步筛选(全部为且操作)
                                  if (where) {
                                      let arr1 = arr.filter(ele => {
                                          let flag = true;
                                          // 只要有一个条件不符合,此条数据就过滤掉
                                          for (let i = 0; i < where.length; i++) {
                                              let filterObj = where[i];
                                              let val = ele[filterObj.key];
                                              if (!val) val = "";
                                              if (typeof val == 'string') val = val.trim();
                                              switch (filterObj.opt) {
                                                  case "==":
                                                      flag = (val == filterObj.value);
                                                      break;
                                                  case "===":
                                                      flag = (val === filterObj.value);
                                                      break;
                                                  case "<":
                                                      flag = (val < filterObj.value);
                                                      break;
                                                  case "<=":
                                                      flag = (val <= filterObj.value);
                                                      break;
                                                  case ">":
                                                      flag = (val > filterObj.value);
                                                      break;
                                                  case ">=":
                                                      flag = (val >= filterObj.value);
                                                      break;
                                                  case "!=":
                                                      flag = (val != filterObj.value);
                                                      break;
                                                  case "!==":
                                                      flag = (val !== filterObj.value);
                                                      break;
                                                  case "include": //包含操作
                                                      flag = (val.includes(filterObj.value));
                                                      break;
                                                  case "beIncluded": //包含操作
                                                      flag = (filterObj.value.includes(val));
                                                      break;
                                                  case "function":  //给定函数操作
                                                      flag = filterObj.value(ele)
                                                      break;
                                                  default: break;
                                              }
                                              if (!flag) {
                                                  break;
                                              }
                                          }
                                          return flag;
                                      });
                                      resolve(arr1);
                                  }
                                  else resolve(arr);
                              }
                          };
                      }
                  }
                  else {  //查询所有数据
                      let arr = [];
                      store.openCursor().onsuccess = function (event) {
                          var cursor = event.target.result; //游标:指向表第一条数据的指针
                          if (cursor) {
                              arr.push(cursor.value);
                              cursor.continue();
                          } else {
                              // console.log('没有更多数据了!');
                              resolve(arr);
                          }
                      };
                  }

              }
              catch (err) {
                  resolve(false);
                  console.error(err);
              }
          })
      }

      /**
       * 作用:修改对象仓库数据,不存在就创建
       * @method updateData
       * @for IndexDBOperation
       * @param {Array} stores 要修改的对象仓库
       * @param {any} data 要修改的数据
       * @return {boolean}  true:修改成功,false:修改失败
       * @author zl-fire 2021/08/17
       * @example
       * //如果版本升级,请下刷新下页面在执行升级操作
       * let DB = new indexDBOperation("testDB4", 1, objectStores);
       *
       * //======================主键本身就是数据元素中的一个字段:recordID===========
       * //修改单条数据
       * let res3 = await DB.updateData(["hello"], { name: "111", age: 111, recordID: 1 });
       *
       * //批量修改数据
       * let res4 = await DB.updateData(["hello"], [
       * { name: "zss111111", age: 180, recordID: 21 },
       * { name: "zss1222222", age: 180, recordID: 22 },
       * { name: "zss1333333", age: 180, recordID: 23 }
       * ]);
       *
       * //======================主键为手动指定的字段thekeyName,不存在于数据仓库结构中===========
       *
       * //修改单条数据
       *  let res3 = await DB.updateData(["hello"], { name: "111", age: 111, recordID: 1 , thekeyName:1 } );
       *
       * //批量修改数据
       * let res4 = await DB.updateData(["hello"], [
       * { name: "zss111111", age: 180, recordID: 21 , thekeyName:2 },
       * { name: "zss1222222", age: 180, recordID: 22 , thekeyName:3 },
       * { name: "zss1333333", age: 180, recordID: 23 , thekeyName:4 }
       * ]);
       *
       *
       **/
      async updateData(stores, data ) {
          let _this = this;
          return new Promise(async function (resolve, reject) {
              try {
                  //先检查数据是否传入
                  if (!(stores && data)) {
                      console.error("stores, data,参数都为必填项!"); return;
                  }
                  //  * 逻辑:先判断数据库是否已经被创建
                  if (_this.db === null) {
                      await _this.getDB();
                  }
                  //创建事物
                  let transaction = _this.db.transaction(stores, "readwrite"); //以可读写方式打开事物,该事务跨越stores中的表(object store)
                  let store = transaction.objectStore(stores[0]); //获取stores数组的第一个元素对象
                  //然后在判断要插入的数据类型:如果是数组,则循环添加插入
                  if (data instanceof Array) {
                      let resArr = [];
                      data.forEach(obj => {
                          let res;
                          // 如果是手动指定thekeyName字段的值为主键
                          if (obj.thekeyName) {
                              let key = obj.thekeyName;
                              delete obj.thekeyName;
                              res = store.put(obj, key);
                          }
                          else {
                              res = store.put(obj);
                          }
                          res.onsuccess = function (event) {
                              resArr.push('数据更新成功');
                              if (resArr.length == data.length) {
                                  resolve(true);
                              }
                          };
                          res.onerror = function (event) {
                              resolve(false);
                              console.error(event);

                          };
                      });

                  }
                  else {
                      let res;
                      let obj = data;
                      // 如果是手动指定thekeyName字段的值为主键
                      if (obj.thekeyName) {
                          let key = obj.thekeyName;
                          delete obj.thekeyName;
                          res = store.put(obj, key);
                      }
                      else {
                          res = store.put(obj);
                      }
                      res.onsuccess = function (event) {
                          resolve(true);
                      };
                      res.onerror = function (event) {
                          resolve(false);
                          console.error(event);
                      };
                  }
              }
              catch (err) {
                  resolve(false);
                  console.error(err);
              }
          })

      }

      /**
       * 作用:删除对象仓库数据,
       * @method deleteData
       * @for IndexDBOperation
       * @param {Array} stores 要删除的对象仓库
       * @param {number|string|Array} data 要删除的数据的key,或者批量key的集合
       * @return {boolean}  true:删除成功,false:删除失败
       * @author zl-fire 2021/08/17
       * @example
       * //如果版本升级,请下刷新下页面在执行升级操作
       * let DB = new indexDBOperation("testDB4", 1, objectStores);
       *
       * //删除主键为23的数据
       * let res5 = await DB.deleteData(["hello"], [23]);
       *
       * //删除表的所有数据
       * let res6 = await DB.deleteData(["hello"]);
       **/
      async deleteData(stores, data) {
          let _this = this;
          return new Promise(async function (resolve, reject) {
              try {
                  //先检查数据是否传入
                  if (!stores) {
                      console.error("stores参数为必填项!"); return;
                  }
                  //  * 逻辑:先判断数据库是否已经被创建
                  if (_this.db === null) {
                      await _this.getDB();
                  }
                  //创建事物
                  let transaction = _this.db.transaction(stores, "readwrite"); //以可读写方式打开事物,该事务跨越stores中的表(object store)
                  let store = transaction.objectStore(stores[0]); //获取stores数组的第一个元素对象
                  //如果未传入data参数,则删除此对象仓库所有的数据
                  if (!data) {
                      store.clear();
                  }
                  //如果是数组,则循环删除
                  else if (data instanceof Array) {
                      let resArr = [];
                      data.forEach(obj => {
                          let res = store.delete(obj);
                          res.onsuccess = function (event) {
                              resArr.push('数据删除成功');
                              if (resArr.length == data.length) {
                                  resolve(true);
                              }
                          };
                          res.onerror = function (event) {
                              resolve(false);
                              console.error(event);

                          };
                      });

                  }
                  //如果是单个值,直接删除
                  else {
                      let res = store.delete(data);
                      res.onsuccess = function (event) {
                          resolve(true);
                      };
                      res.onerror = function (event) {
                          resolve(false);
                          console.error(event);
                      };
                  }
              }
              catch (err) {
                  resolve(false);
                  console.error(err);
              }
          })

      }

      /**
       * 作用:关闭数据链接,
       * @method close
       * @for IndexDBOperation
       * @return {boolean}  true:成功,false:失败
       * @author zl-fire 2021/08/17
       * @example
       * //如果版本升级,请下刷新下页面在执行升级操作
       * let DB = new indexDBOperation("testDB4", 1, objectStores);
       *
       * //关闭数据库链接(当数据库的链接关闭后,对他的操作就不再有效)
       * let res7 = await DB.close();
       **/
      async close() {
          let _this = this;
          return new Promise(async function (resolve, reject) {
              try {
                  //  * 逻辑:先判断数据库是否已经被创建
                  if (_this.db === null) {
                      await _this.getDB();
                  }
                  _this.db.close();
              }
              catch (err) {
                  resolve(false);
                  console.error(err);
              }
          })
      }

      /**
       * 作用:删除数据库,如果没传参就删除本身数据库,否则删除指定数据库
       * @method deleteDataBase
       * @for IndexDBOperation
       * @author zl-fire 2021/08/17
       * @example
       * //如果版本升级,请下刷新下页面在执行升级操作
       * let DB = new indexDBOperation("testDB4", 1, objectStores);
       *
       * //删除数据库,如果没传参就删除本身数据库,否则删除指定数据库
       * let res8 = await DB.deleteDataBase();//删除后,可能要刷新下才能看到application中indexdb数据库的变化
       **/
      async deleteDataBase(name) {
          name = name ? name : this.dbName;
          return new Promise(async function (resolve, reject) {
              try {

                  window.indexedDB.deleteDatabase(name);
                  resolve(true);
              }
              catch (err) {
                  resolve(false);
                  console.error(err);
              }
          })

      }
  }

  return IndexDBOperation;

})));






(function() {

    var possibleNextData
    var pageIndex = -1
    var argumentsInStorage = sessionStorage.getItem('arguments')
    /*
    劫持XHR为本地数据库数据
    */
    const xhrOpen = XMLHttpRequest.prototype.open;
if(document.location.pathname=='/account/history'){
        console.log('ok')
    XMLHttpRequest.prototype.open = function() {
      if (arguments[1].includes("https://api.bilibili.com/x/web-interface/history")) {
        const xhr = this;
        const getter = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText').get;
        Object.defineProperty(xhr, 'responseText', {
          get: () => {
            let result = getter.call(xhr);
            result_json = JSON.parse(result)
            //result_json.data.list[0].title = '劫持后的数据'
            result_json = possibleNextData||result_json
            result = JSON.stringify(result_json)
            if(pageIndex==-1){
                result = sessionStorage.getItem('frontTwenty')
                return result;
            }
            if(argumentsInStorage != ''){
                result = JSON.stringify(possibleNextData)
                return result
            }


          }
        });
      }
      return xhrOpen.apply(this, arguments);
    };
}
    // Your code here...
    var whole_data = undefined
    setInterval(()=>{
        newIndex = getPageIndex()-1
        if(newIndex != pageIndex ){
            pageIndex = newIndex
            updatePossibleNextData()
        }
    },500)
    setTimeout(async()=>{
        await updatePossibleWholeData()
        await updatePossibleNextData()
        },3000)
    setTimeout(()=>{jQuery('#loading').hide()},4000)
    window.onload = async function(){
      await initMyDB()
      await navigator.storage.persist()
      if(document.location.pathname != "/account/history"){
       list = await getNewHistory()
       await updateMyDB(list)
       setInterval(()=>{collect_info()},2000)
       await updateInfoToDB()

       }
          /*来点UI*/
        GM_addStyle('.btnControl{background-color:#1E90FF;position:relative}')
        GM_addStyle('.controlArea{position:fixed;left:50%;top:50%;transform: translate(-50%, -50%);width:450px;height:300px;z-index:999;padding:0 10px;}')
        GM_addStyle(`
        #loading{
            position:fixed;left:50%;top:20%;
            color:#7FFFD4;font-size:15px;font-weight:500;
            }
        .controlArea input{
            max-width:40px;
        }
        .controlArea fieldset:nth-child(2) input{
            max-width:100px;
        }
        fieldset{
            padding:5px;
           margin:10px;
        }
        .bigBtn{
            height:45px;
            position:relative;
         }
        .bigBtn button{
            position: absolute;
            top: 50%; left: 50%;
            background: #b4a078;
            color: white;
            padding: .3em 1em .5em;
            border-radius: 3px;
            box-shadow: 0 0 .5em #b4a078;
            transform: translate(-50%, -50%);
        }
        .controlArea{
            display:none;
            background-color:#6495EDDD;
        }
        `)
        let showControl = '<button class="btnControl">扩展功能</button><a id="loading" >查找数据中</a>'
        let controlArea = '<div class="controlArea"></div>'
        let timeInput = `<fieldset>
        <legend>观看情况筛选</legend>
        <div>在<input></input>日<input></input>小时前观看的视频</div>
        <div>观看设备是:
        <input type="checkbox" name="from" value="phone" checked>手机端</input>
        <input type="checkbox" name="from" value="web" checked>网页端</input>
        <input type="checkbox" name="from" value="other" checked>其他</input>
        </div>
        </fieldset>`
        let keyWordInput = `
        <fieldset>
        <legend>视频关键词搜索</legend>
        <input></input>标题关键词搜索(支持多个关键词,如"空洞|速通")</div><br>
        <input type="checkbox" name="plus" value="plus" false>关键词搜索范围扩展到标签、up主名称、简介(仅适用于web端观看)</input><br>
        <input type="checkbox" name="plus" value="commentPlus" false>扩展到热评(仅web端)</input>
        </fieldset>`
        let watchFrom = `
        <fieldset><legend>视频信息筛选</legend>
        <div>视频长度大于<input></input>分钟且小于<input></input>分钟/<div>
        <div>视频类型:
        <input type="checkbox" name="business" value="archive" checked>稿件</input>
        <input type="checkbox" name="business" value="pgc" checked>番剧</input>
        <input type="checkbox" name="business" value="live" checked>直播</input>
        <input type="checkbox" name="business" value="article" checked>文章</input>
        </div>
        </fieldset>`
        let submitBtn = '<div class="bigBtn"><button>提交</button></div>'
        jQuery('.b-head-search').before(showControl,controlArea)
        jQuery('.controlArea').append(timeInput,keyWordInput,watchFrom,submitBtn)
        jQuery('.btnControl').click(function(){
            jQuery('.controlArea').toggle()
        })
        jQuery('.bigBtn').click(function(){
            let day = jQuery('.controlArea fieldset:nth-child(1) div input:nth-child(1)').val()
            let hour = jQuery('.controlArea fieldset:nth-child(1) div input:nth-child(2)').val()
            let keyWord = jQuery('.controlArea fieldset:nth-child(2) input:nth-child(2)').val()
            let durationStart = jQuery('.controlArea fieldset:nth-child(3) div input:nth-child(1)').val()
            let durationEnd = jQuery('.controlArea fieldset:nth-child(3) div input:nth-child(2)').val()
            let keyWordPlus = false
            let commentPlus = false
            let fromList = []
            let businessList = []
            jQuery("input[name=from]:checked").each(function(){
                fromList.push(this.value);
            });
            jQuery("input[name=business]:checked").each(function(){
                businessList.push(this.value);
            });
            jQuery("input[name=plus]:checked").each(function(){
                if(this.value == 'plus'){
                    keyWordPlus = true
                }
                if(this.value == 'commentPlus'){
                    commentPlus = true
                }
            });
            searchDBWithForm(day,hour,keyWord,fromList,businessList,keyWordPlus,commentPlus,durationStart,durationEnd)
        })

    }
   /* 提供前20条数据用于初次渲染*/
    async function searchDBWithForm(day,hour,keyWord,fromList,businessList,keyWordPlus,commentPlus,durationStart,durationEnd){
        let [query1,query2] = queryCreate(arguments)
        let res = await searchDB(query1,query2,ps=50000)
        frontTwenty =  packingHistory(res,0)
        sessionStorage.setItem('frontTwenty',JSON.stringify(frontTwenty))
        sessionStorage.setItem('arguments',JSON.stringify(arguments))
        window.location.reload()
    }
    /* 加载接下来的20条数据 ,*/
    async function updatePossibleNextData(){
       console.log('I want to update possibleNextData with loading,with the whole_data is' + typeof(whole_data))
    if(!whole_data){
       let arg = JSON.parse(argumentsInStorage)
       let [query1,query2] = queryCreate([arg[0],arg[1],arg[2],arg[3],arg[4],arg[5],arg[6],arg[7],arg[8]])
      let search = await searchDB(query1,query2,ps=50000)
      possibleNextData = packingHistory(search,20*((pageIndex?pageIndex:0)+1))
     }
    else{
        possibleNextData = packingHistory(whole_data,20*((pageIndex?pageIndex:0)+1))
     }
     console.log('Next 20 may be',possibleNextData)
    }
   /*加载得到数据库中所有符合条件的数据 */
    async function updatePossibleWholeData(){
        let arg = JSON.parse(argumentsInStorage)
        let [query1,query2] = queryCreate([arg[0],arg[1],arg[2],arg[3],arg[4],arg[5],arg[6],arg[7],arg[8]])
       whole_data= await searchDB(query1,query2,ps=500000)
       console.log('whole_data now loaded successfully',whole_data)
    }




})();

/*生成数据库查询的参数 */
function queryCreate(args){
    let [day,hour,keyWord,fromList,businessList,keyWordPlus,commentPlus,durationStart,durationEnd] = args
    let seconds = 0
    if(day != '') {seconds += 60*60*24*parseInt(day)}
    if(hour != ''){seconds += 60*60*parseInt(hour)}
    console.log(day,hour,keyWord,fromList,businessList,keyWordPlus)
    let query1 = {name:'viewAtIndex',value:parseInt(+new Date()/1000)-seconds}


    let query2 = []
    if(fromList.length>0){
        let arr = []
        if(fromList.includes('phone')){
            arr = arr.concat([1,3,5,7])
        }
        if(fromList.includes('web')){
            arr = arr.concat([2])
        }
        if(fromList.includes('other')){
            arr = arr.concat([4,6,8,9,33,0])
        }
        query2.push({key:'dt',value:arr,opt:'beIncluded'})
    }
    if(businessList.length>0){
        query2.push({key:'business',value:businessList,opt:'beIncluded'})
    }
    function plusFunction(ele){
        let resFlag = false
        let checkString = ele.title
        if(keyWordPlus){checkString = checkString +  ele.author_name + ele.tags + ele.des }
        if(commentPlus){checkString = checkString + ele.comments }
        if(!keyWord.includes('|')){
            if(checkString.includes(keyWord)){
                resFlag = true
            }
        }else{
            let keyArr = keyWord.split('|')
            for(let i=0;i<keyArr.length;i++){
                resFlag = checkString.includes(keyArr[i])
                if(!resFlag) {return false}
            }
        }
        return resFlag
    }
    if(keyWord != ''){
        query2.push({key:'business',value:plusFunction,opt:'function'})
    }
    if(durationStart!=''){
        query2.push({key:'duration',value:durationStart*60,opt:'>'})
    }
    if(durationEnd!=''){
        query2.push({key:'duration',value:durationEnd*60,opt:'<'})
    }
    console.log('queryCreate Function',args,query1,query2)
    return [query1,query2]
}
/*
B站的历史记录api来更新最新的历史数据到数据库
*/
async function getNewHistory(){
  var wholeList = []
  var page = await getHistoryApi(0,0,'')
  console.log(page)
  wholeList = wholeList.concat(page.data.list)
  stopFlag = await ifExistInDB(page.data.list)
  for(let i=0;i<20&&!stopFlag;i++){
    cursor = page.data.cursor
    page = await getHistoryApi(cursor.max,cursor.view_at,cursor.business)
    wholeList = wholeList.concat(page.data.list)
    stopFlag = await ifExistInDB(page.data.list)
    if(stopFlag){
      break
    }
  }
  return wholeList
}
/* B站api调用 */
function getHistoryApi(max,view_at,business){
let myPromise = new Promise(function(resolve,reject){
  jQuery.support.cors = true;
  jQuery.ajax({
            url: `https://api.bilibili.com/x/web-interface/history/cursor?max=${max}&view_at=${view_at}&business=${business}`,
            type: "get",
            dataType: "json",
            xhrFields: { withCredentials: true },
            success: function(res) {
                console.log(res)
                resolve(res)
              }
            })
 })
 return myPromise
}
/* 防止过多调用api */
async function ifExistInDB(list){
    let data = {}
    for(let i=0;i<list.length;i++){
      data = await getMyDBById(list[i].history.oid)
      if(data&&data.view_at==list[i].view_at){
        return true
      }
    }
    return false
}
/* 将数据库数据包装成xhr结果的格式 */
function packingHistory(list,i){
    let listCut = list.slice(i,i+20)
    if(listCut.length==0){
        return []
    }
    let res = {code:0,ttl:1,message:'0',data:{tab:[{type: "archive", name: "视频"}, {type: "live", name: "直播"}, {type: "article", name: "专栏"}]}}
    res.data.list = listCut
    last = listCut[listCut.length-1]
    res.data.cursor = {
      business:last.history.business,
      max:last.history.oid,
      ps:20,
      view_at:last.view_at
    }
    return res
}
/*
处理一些页面的基本信息
*/
function getPageIndex(){
    return Math.ceil(jQuery('.history-list').children().length/20)
}
/*
通过开源库操作indexDB
*/
async function initMyDB(){
  let IndexDBOpt=window["zl-indexdb"];
  let dbName='bilibiliHistoryDB';
  let dbVersion=1;
  let objectStores = [
    {
        objectStoreName: "history",//表名
        type: 1, //0:表示把已存在的删除,然后重新创建。 1:表示如果不存在才重新创建,2:表示只删除,不再进行重新创建
        keyMode:  { keyPath: 'oid' },
        indexs: [ // 创建索引信息
            { indexName: "oidIndex", fieldName: "oid", only: { unique: false } },//索引名,字段名,索引属性值是否唯一
            { indexName: "viewAtIndex", fieldName: "view_at", only: { unique: false } },
            { indexName: "titleIndex", fieldName: "title", only: { unique: false } },
            { indexName: "businessIndex", fieldName: "business", only: { unique: false } },
        ]
    },
    {
        objectStoreName: "storage",//表名
        type: 1, //0:表示把已存在的删除,然后重新创建。 1:表示如果不存在才重新创建,2:表示只删除,不再进行重新创建
        keyMode:  { keyPath: 'bvid' },
        indexs: [ // 创建索引信息

        ]
    }
  ]
  window.DB = new IndexDBOpt(dbName, dbVersion, objectStores);
}
async function updateMyDB(list){
  for(let i=0;i<list.length;i++){
    list[i] = {...list[i],...list[i].history}
    let res = await getMyDBById(list[i].oid)
    if(res){res.view_at = list[i].view_at; list[i]=res}
  }
  await DB.updateData(['history'],list)
}
async function getMyDBById(oid){
    let res = await DB.queryBykeypath(['history'],oid)
    return res
}
async function searchDB(query,queryList,ps){
  let res = await DB.queryByIndex(['history'],query,queryList,ps)
  return res
}



/*
在观看视频时,提供页面的信息
*/
async function collect_info(){
    var bvid = document.location.pathname.match(/BV\w{1,15}/g)[0]
    var tags = jQuery("meta[name='keywords']").attr("content"); //描述
    var des = jQuery('#v_desc > div.desc-info.desc-v2 > span').text() //简介
    var comments = jQuery(' div.content-warp > div.root-reply > span').text().slice(1,300) //评论区
    await DB.updateData(['storage'],{bvid,tags,des,comments})
}
async function updateInfoToDB(){
    let res = await DB.queryByIndex(['storage'])
    for(let i=0;i<res.length;i++){
        let flag = updateOneVideo(res[i])
        if(flag) await DB.deleteData(["storage"], [res[i].bvid]);
    }
}
async function updateOneVideo(e){
    let {bvid,tags,des,comments} = e
    let res = await searchDB({name:'viewAtIndex',value:parseInt(+new Date()/1000)},[{key:'bvid',value:bvid,opt:'=='}],10000)
    console.log(res)
    if(res.length){
        console.log(res)
        res = res[0]
        res.tags = tags
        res.des = des
        res.comments = comments
        await DB.updateData(['history'],res)
        return true
    }
    return false
}