Spaces:
Sleeping
Sleeping
Commit
·
bf48544
1
Parent(s):
88f57ce
init
Browse files- README.md +0 -10
- app.py +1212 -162
- requirements.txt +4 -6
- shared.py +0 -6
- styles.css +0 -12
- tips.csv +0 -245
README.md
CHANGED
|
@@ -9,13 +9,3 @@ license: mit
|
|
| 9 |
short_description: 在线文件云端中转到你的网盘
|
| 10 |
---
|
| 11 |
|
| 12 |
-
This is a templated Space for [Shiny for Python](https://shiny.rstudio.com/py/).
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
To get started with a new app do the following:
|
| 16 |
-
|
| 17 |
-
1) Install Shiny with `pip install shiny`
|
| 18 |
-
2) Create a new app with `shiny create`
|
| 19 |
-
3) Then run the app with `shiny run --reload`
|
| 20 |
-
|
| 21 |
-
To learn more about this framework please see the [Documentation](https://shiny.rstudio.com/py/docs/overview.html).
|
|
|
|
| 9 |
short_description: 在线文件云端中转到你的网盘
|
| 10 |
---
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1,162 +1,1212 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
在线文件转存到网盘 - 基于 FastAPI 的现代化界面
|
| 4 |
+
通过云端高速通道实现文件转存到网盘中
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import uuid
|
| 9 |
+
import os
|
| 10 |
+
import io
|
| 11 |
+
import zipfile
|
| 12 |
+
import requests
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
from urllib.parse import urlparse, unquote
|
| 15 |
+
|
| 16 |
+
from fastapi import FastAPI, Request, Query
|
| 17 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 18 |
+
from pydantic import BaseModel
|
| 19 |
+
import uvicorn
|
| 20 |
+
|
| 21 |
+
DEFAULT_TOKEN_Q = ""
|
| 22 |
+
|
| 23 |
+
ACCEPT_VALUE = os.environ.get("ACCEPT_VALUE", "")
|
| 24 |
+
API_VERSION_KEY = os.environ.get("API_VERSION_KEY", "")
|
| 25 |
+
ACCESS_TOKEN = os.environ.get("ACCESS_TOKEN_G", "")
|
| 26 |
+
API_BASE_URL = os.environ.get("API_BASE_URL", f"")
|
| 27 |
+
QUARK_COOKIE = os.environ.get("ACCESS_TOKEN_Q", DEFAULT_TOKEN_Q)
|
| 28 |
+
|
| 29 |
+
ENABLE_TEST_DATA = False
|
| 30 |
+
|
| 31 |
+
def get_remote_processor():
|
| 32 |
+
|
| 33 |
+
token = ACCESS_TOKEN
|
| 34 |
+
base_url = API_BASE_URL
|
| 35 |
+
|
| 36 |
+
if not all([token, base_url]):
|
| 37 |
+
return None
|
| 38 |
+
return RemoteTaskProcessor(token, base_url)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class RemoteTaskProcessor:
|
| 42 |
+
def __init__(self, token, base_url):
|
| 43 |
+
self.token = token
|
| 44 |
+
self.base_url = base_url
|
| 45 |
+
self.headers = {
|
| 46 |
+
"Authorization": f"Bearer {token}",
|
| 47 |
+
"Accept": ACCEPT_VALUE,
|
| 48 |
+
API_VERSION_KEY: "2022-11-28"
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
def generate_task_id(self):
|
| 52 |
+
date_str = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 53 |
+
short_uuid = uuid.uuid4().hex[:12]
|
| 54 |
+
return f"task_{date_str}_{short_uuid}"
|
| 55 |
+
|
| 56 |
+
def exec_task(self, task_file, inputs, trace_id):
|
| 57 |
+
url = f"{self.base_url}/actions/workflows/{task_file}/dispatches"
|
| 58 |
+
dispatch_inputs = {"trace_id": trace_id, **inputs}
|
| 59 |
+
data = {"ref": "main", "inputs": dispatch_inputs}
|
| 60 |
+
response = requests.post(url, headers=self.headers, json=data)
|
| 61 |
+
return response.status_code == 204, response
|
| 62 |
+
|
| 63 |
+
def find_task_by_task_id(self, task_file, trace_id):
|
| 64 |
+
url = f"{self.base_url}/actions/workflows/{task_file}/runs"
|
| 65 |
+
params = {"event": "workflow_dispatch", "branch": "main", "per_page": 20}
|
| 66 |
+
resp = requests.get(url, headers=self.headers, params=params)
|
| 67 |
+
resp.raise_for_status()
|
| 68 |
+
runs = resp.json().get("workflow_runs", [])
|
| 69 |
+
for run in runs:
|
| 70 |
+
run_name = run.get("name", "")
|
| 71 |
+
display_title = run.get("display_title", "")
|
| 72 |
+
if trace_id in run_name or trace_id in display_title:
|
| 73 |
+
return run
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
def get_task_status(self, task_id):
|
| 77 |
+
url = f"{self.base_url}/actions/runs/{task_id}"
|
| 78 |
+
response = requests.get(url, headers=self.headers)
|
| 79 |
+
if response.status_code != 200:
|
| 80 |
+
return None
|
| 81 |
+
return response.json()
|
| 82 |
+
|
| 83 |
+
def get_result(self, run_id, artifact_name="result"):
|
| 84 |
+
url = f"{self.base_url}/actions/runs/{run_id}/artifacts"
|
| 85 |
+
response = requests.get(url, headers=self.headers)
|
| 86 |
+
if response.status_code != 200:
|
| 87 |
+
return None
|
| 88 |
+
artifacts = response.json().get("artifacts", [])
|
| 89 |
+
target = None
|
| 90 |
+
for a in artifacts:
|
| 91 |
+
if a["name"] == artifact_name:
|
| 92 |
+
target = a
|
| 93 |
+
break
|
| 94 |
+
if not target:
|
| 95 |
+
return None
|
| 96 |
+
download_url = f"{self.base_url}/actions/artifacts/{target['id']}/zip"
|
| 97 |
+
response = requests.get(download_url, headers=self.headers)
|
| 98 |
+
if response.status_code != 200:
|
| 99 |
+
return None
|
| 100 |
+
try:
|
| 101 |
+
with zipfile.ZipFile(io.BytesIO(response.content)) as zf:
|
| 102 |
+
if "result.json" in zf.namelist():
|
| 103 |
+
return json.loads(zf.read("result.json").decode("utf-8"))
|
| 104 |
+
except Exception:
|
| 105 |
+
pass
|
| 106 |
+
return None
|
| 107 |
+
|
| 108 |
+
def is_valid_url(url: str) -> bool:
|
| 109 |
+
"""初步判断是否是有效的 http/https 下载链接"""
|
| 110 |
+
if not url:
|
| 111 |
+
return False
|
| 112 |
+
try:
|
| 113 |
+
result = urlparse(url.strip())
|
| 114 |
+
return all([result.scheme, result.netloc]) and result.scheme in ["http", "https"]
|
| 115 |
+
except Exception:
|
| 116 |
+
return False
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def extract_filename_from_url(url):
|
| 120 |
+
|
| 121 |
+
if not url or not url.strip():
|
| 122 |
+
return ""
|
| 123 |
+
try:
|
| 124 |
+
parsed = urlparse(url.strip())
|
| 125 |
+
path = unquote(parsed.path)
|
| 126 |
+
path = path.rstrip("/")
|
| 127 |
+
filename = os.path.basename(path)
|
| 128 |
+
if filename and "." in filename:
|
| 129 |
+
return filename
|
| 130 |
+
if filename:
|
| 131 |
+
return filename
|
| 132 |
+
except Exception:
|
| 133 |
+
pass
|
| 134 |
+
return ""
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# ============================================================
|
| 140 |
+
# 核心刷新逻辑
|
| 141 |
+
# ============================================================
|
| 142 |
+
def _do_refresh(task_list, trigger):
|
| 143 |
+
updated_count = 0
|
| 144 |
+
for task in task_list:
|
| 145 |
+
if task["status"] not in ["正在转存", "未提交"]:
|
| 146 |
+
continue
|
| 147 |
+
run_id_str = task.get("run_id", "")
|
| 148 |
+
if not run_id_str:
|
| 149 |
+
run = trigger.find_task_by_task_id("upload.yml", task["trace_id"])
|
| 150 |
+
if run:
|
| 151 |
+
task["run_id"] = str(run["id"])
|
| 152 |
+
run_id_str = str(run["id"])
|
| 153 |
+
else:
|
| 154 |
+
continue
|
| 155 |
+
try:
|
| 156 |
+
run_data = trigger.get_task_status(int(run_id_str))
|
| 157 |
+
except (ValueError, TypeError):
|
| 158 |
+
continue
|
| 159 |
+
if not run_data:
|
| 160 |
+
continue
|
| 161 |
+
status = run_data.get("status")
|
| 162 |
+
conclusion = run_data.get("conclusion")
|
| 163 |
+
if status == "completed":
|
| 164 |
+
if conclusion == "success":
|
| 165 |
+
result = trigger.get_result(int(run_id_str))
|
| 166 |
+
if result and isinstance(result, dict):
|
| 167 |
+
task["status"] = "已转存"
|
| 168 |
+
task["share_url"] = result.get("share_url", "")
|
| 169 |
+
else:
|
| 170 |
+
task["status"] = "已转存"
|
| 171 |
+
else:
|
| 172 |
+
task["status"] = "失败"
|
| 173 |
+
updated_count += 1
|
| 174 |
+
return updated_count
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _has_active_tasks(task_list):
|
| 178 |
+
return any(t["status"] in ["正在转存", "未提交"] for t in task_list)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
# ============================================================
|
| 182 |
+
# FastAPI 应用
|
| 183 |
+
# ============================================================
|
| 184 |
+
app = FastAPI(title="CloudFileRelay|Online Files to Cloud Drive")
|
| 185 |
+
|
| 186 |
+
# 会话存储
|
| 187 |
+
sessions: dict = {}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def get_or_create_session(request: Request) -> tuple:
|
| 191 |
+
sid = request.cookies.get("sid", "")
|
| 192 |
+
if sid and sid in sessions:
|
| 193 |
+
return sid, sessions[sid]
|
| 194 |
+
sid = uuid.uuid4().hex
|
| 195 |
+
|
| 196 |
+
test_tasks = []
|
| 197 |
+
if ENABLE_TEST_DATA:
|
| 198 |
+
# 初始化一些测试数据以展示分页效果
|
| 199 |
+
for i in range(45):
|
| 200 |
+
test_tasks.append({
|
| 201 |
+
"trace_id": f"test_{20240208}_{i:03d}",
|
| 202 |
+
"run_id": str(123456 + i),
|
| 203 |
+
"filename": f"测试文件_{i:03d}.mp4",
|
| 204 |
+
"url": f"https://example.com/file_{i:03d}.mp4",
|
| 205 |
+
"status": "已转存" if i % 3 != 0 else "失败",
|
| 206 |
+
"share_url": f"https://quark.cn/s/{"abcdefghijk"[i%11]*12}" if i % 3 != 0 else "",
|
| 207 |
+
"created_at": f"2026-02-08 10:{i//60:02d}:{i%60:02d}",
|
| 208 |
+
})
|
| 209 |
+
|
| 210 |
+
sessions[sid] = test_tasks
|
| 211 |
+
return sid, sessions[sid]
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def json_resp(data: dict, sid: str) -> JSONResponse:
|
| 215 |
+
resp = JSONResponse(content=data)
|
| 216 |
+
resp.set_cookie("sid", sid, max_age=86400 * 7, httponly=True, samesite="lax")
|
| 217 |
+
return resp
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
# ---- 请求模型 ----
|
| 221 |
+
class SubmitRequest(BaseModel):
|
| 222 |
+
url: str
|
| 223 |
+
filename: str = ""
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
class QueryRequest(BaseModel):
|
| 227 |
+
trace_id: str
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
# ---- 页面 ----
|
| 231 |
+
@app.get("/", response_class=HTMLResponse)
|
| 232 |
+
async def index(request: Request):
|
| 233 |
+
sid, _ = get_or_create_session(request)
|
| 234 |
+
resp = HTMLResponse(content=HTML_TEMPLATE)
|
| 235 |
+
resp.set_cookie("sid", sid, max_age=86400 * 7, httponly=True, samesite="lax")
|
| 236 |
+
return resp
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
# ---- API ----
|
| 240 |
+
@app.get("/api/extract-filename")
|
| 241 |
+
async def api_extract_filename(url: str = Query("")):
|
| 242 |
+
return {"filename": extract_filename_from_url(url)}
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
@app.post("/api/submit")
|
| 246 |
+
async def api_submit(req: SubmitRequest, request: Request):
|
| 247 |
+
sid, task_list = get_or_create_session(request)
|
| 248 |
+
|
| 249 |
+
if not req.url or not req.url.strip():
|
| 250 |
+
return json_resp({"success": False, "message": "请输入下载链接", "tasks": task_list}, sid)
|
| 251 |
+
|
| 252 |
+
url_to_submit = req.url.strip()
|
| 253 |
+
if not is_valid_url(url_to_submit):
|
| 254 |
+
return json_resp({
|
| 255 |
+
"success": False,
|
| 256 |
+
"message": "转存失败,请输入有效的下载链接地址",
|
| 257 |
+
"tasks": task_list
|
| 258 |
+
}, sid)
|
| 259 |
+
|
| 260 |
+
# 检查是否已存在相同链接且处于活跃状态的任务
|
| 261 |
+
|
| 262 |
+
for task in task_list:
|
| 263 |
+
if task.get("url") == url_to_submit and task.get("status") in ["正在转存", "未提交"]:
|
| 264 |
+
return json_resp({
|
| 265 |
+
"success": False,
|
| 266 |
+
"message": "该任务已在转存中,请耐心等待,无需重复提交。",
|
| 267 |
+
"tasks": task_list
|
| 268 |
+
}, sid)
|
| 269 |
+
|
| 270 |
+
trigger = get_remote_processor()
|
| 271 |
+
if not trigger:
|
| 272 |
+
return json_resp({
|
| 273 |
+
"success": False,
|
| 274 |
+
"message": "配置缺失!",
|
| 275 |
+
"tasks": task_list
|
| 276 |
+
}, sid)
|
| 277 |
+
|
| 278 |
+
local_file = req.filename.strip() if req.filename and req.filename.strip() else extract_filename_from_url(req.url)
|
| 279 |
+
trace_id = trigger.generate_task_id()
|
| 280 |
+
|
| 281 |
+
cookie = QUARK_COOKIE
|
| 282 |
+
inputs = {
|
| 283 |
+
"url": req.url.strip(),
|
| 284 |
+
"local_file": local_file,
|
| 285 |
+
"cookie": cookie
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
success, resp = trigger.exec_task("upload.yml", inputs, trace_id)
|
| 289 |
+
if not success:
|
| 290 |
+
error_detail = ""
|
| 291 |
+
try:
|
| 292 |
+
error_detail = resp.text[:300]
|
| 293 |
+
except Exception:
|
| 294 |
+
pass
|
| 295 |
+
return json_resp({
|
| 296 |
+
"success": False,
|
| 297 |
+
"message": f"任务触发失败 (HTTP {resp.status_code})\n{error_detail}",
|
| 298 |
+
"tasks": task_list
|
| 299 |
+
}, sid)
|
| 300 |
+
|
| 301 |
+
task = {
|
| 302 |
+
"trace_id": trace_id,
|
| 303 |
+
"run_id": "",
|
| 304 |
+
"filename": local_file,
|
| 305 |
+
"url": req.url.strip(),
|
| 306 |
+
"status": "正在转存",
|
| 307 |
+
"share_url": "",
|
| 308 |
+
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 309 |
+
}
|
| 310 |
+
task_list.append(task)
|
| 311 |
+
|
| 312 |
+
return json_resp({
|
| 313 |
+
"success": True,
|
| 314 |
+
"message": f"转存任务已提交!\n任务 ID: {trace_id}\n文件名: {local_file}",
|
| 315 |
+
"task_id": trace_id,
|
| 316 |
+
"tasks": task_list
|
| 317 |
+
}, sid)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
@app.get("/api/tasks")
|
| 321 |
+
async def api_tasks(request: Request):
|
| 322 |
+
sid, task_list = get_or_create_session(request)
|
| 323 |
+
return json_resp({"tasks": task_list}, sid)
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
@app.post("/api/refresh")
|
| 327 |
+
async def api_refresh(request: Request):
|
| 328 |
+
sid, task_list = get_or_create_session(request)
|
| 329 |
+
|
| 330 |
+
trigger = get_remote_processor()
|
| 331 |
+
if not trigger:
|
| 332 |
+
return json_resp({"tasks": task_list, "message": "配置缺失", "all_done": True}, sid)
|
| 333 |
+
|
| 334 |
+
if not task_list:
|
| 335 |
+
return json_resp({"tasks": task_list, "message": "暂无任务", "all_done": True}, sid)
|
| 336 |
+
|
| 337 |
+
count = _do_refresh(task_list, trigger)
|
| 338 |
+
now = datetime.now().strftime("%H:%M:%S")
|
| 339 |
+
all_done = not _has_active_tasks(task_list)
|
| 340 |
+
|
| 341 |
+
if count > 0:
|
| 342 |
+
msg = f"[{now}] 已更新 {count} 个任务的状态"
|
| 343 |
+
else:
|
| 344 |
+
msg = f"[{now}] 正在转存"
|
| 345 |
+
if all_done and task_list:
|
| 346 |
+
msg += " · 所有任务已完成"
|
| 347 |
+
|
| 348 |
+
return json_resp({"tasks": task_list, "message": msg, "all_done": all_done}, sid)
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
@app.post("/api/clear")
|
| 352 |
+
async def api_clear(request: Request):
|
| 353 |
+
sid, _ = get_or_create_session(request)
|
| 354 |
+
sessions[sid] = []
|
| 355 |
+
return json_resp({"success": True, "tasks": [], "message": "任务列表已清空"}, sid)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
@app.post("/api/query")
|
| 359 |
+
async def api_query(req: QueryRequest, request: Request):
|
| 360 |
+
sid, task_list = get_or_create_session(request)
|
| 361 |
+
|
| 362 |
+
if not req.trace_id or not req.trace_id.strip():
|
| 363 |
+
return json_resp({"success": False, "message": "请输入任务 ID", "tasks": task_list}, sid)
|
| 364 |
+
|
| 365 |
+
trigger = get_remote_processor()
|
| 366 |
+
if not trigger:
|
| 367 |
+
return json_resp({"success": False, "message": "配置缺失", "tasks": task_list}, sid)
|
| 368 |
+
|
| 369 |
+
trace_id = req.trace_id.strip()
|
| 370 |
+
|
| 371 |
+
# 在本地任务列表中查找 run_id
|
| 372 |
+
run_id = None
|
| 373 |
+
for task in task_list:
|
| 374 |
+
if task.get("trace_id") == trace_id and task.get("run_id"):
|
| 375 |
+
try:
|
| 376 |
+
run_id = int(task["run_id"])
|
| 377 |
+
except (ValueError, TypeError):
|
| 378 |
+
pass
|
| 379 |
+
break
|
| 380 |
+
|
| 381 |
+
if not run_id:
|
| 382 |
+
run = trigger.find_task_by_task_id("upload.yml", trace_id)
|
| 383 |
+
if not run:
|
| 384 |
+
return json_resp({
|
| 385 |
+
"success": False,
|
| 386 |
+
"message": f"未找到任务 ID: {trace_id}\n可能任务尚未被创建,请稍后再试",
|
| 387 |
+
"tasks": task_list
|
| 388 |
+
}, sid)
|
| 389 |
+
run_id = run["id"]
|
| 390 |
+
for task in task_list:
|
| 391 |
+
if task.get("trace_id") == trace_id:
|
| 392 |
+
task["run_id"] = str(run_id)
|
| 393 |
+
|
| 394 |
+
run_data = trigger.get_task_status(run_id)
|
| 395 |
+
if not run_data:
|
| 396 |
+
return json_resp({
|
| 397 |
+
"success": False,
|
| 398 |
+
"message": f"无法获取任务状态\n任务 ID: {trace_id}",
|
| 399 |
+
"tasks": task_list
|
| 400 |
+
}, sid)
|
| 401 |
+
|
| 402 |
+
status = run_data.get("status")
|
| 403 |
+
conclusion = run_data.get("conclusion")
|
| 404 |
+
html_url = run_data.get("html_url", "")
|
| 405 |
+
|
| 406 |
+
if status != "completed":
|
| 407 |
+
status_map = {
|
| 408 |
+
"queued": "排队中", "in_progress": "执行中",
|
| 409 |
+
"waiting": "等待中", "requested": "已请求", "pending": "等待中"
|
| 410 |
+
}
|
| 411 |
+
status_cn = status_map.get(status, status)
|
| 412 |
+
return json_resp({
|
| 413 |
+
"success": True,
|
| 414 |
+
"message": f"任务正在执行中,请稍后再查询\n\n任务 ID: {trace_id}\n当前状态: {status_cn}",
|
| 415 |
+
"tasks": task_list
|
| 416 |
+
}, sid)
|
| 417 |
+
|
| 418 |
+
if conclusion == "success":
|
| 419 |
+
result = trigger.get_result(run_id)
|
| 420 |
+
for task in task_list:
|
| 421 |
+
if task.get("trace_id") == trace_id:
|
| 422 |
+
if result and isinstance(result, dict):
|
| 423 |
+
task["status"] = "已转存"
|
| 424 |
+
task["share_url"] = result.get("share_url", "")
|
| 425 |
+
else:
|
| 426 |
+
task["status"] = "已转存"
|
| 427 |
+
|
| 428 |
+
if result and isinstance(result, dict):
|
| 429 |
+
share_url = result.get("share_url", "无")
|
| 430 |
+
local_file = result.get("local_file", "无")
|
| 431 |
+
result_status = result.get("status", "unknown")
|
| 432 |
+
return json_resp({
|
| 433 |
+
"success": True,
|
| 434 |
+
"message": (
|
| 435 |
+
f"任务已完成!\n\n"
|
| 436 |
+
f"任务 ID: {trace_id}\n"
|
| 437 |
+
f"状态: {result_status}\n"
|
| 438 |
+
f"文件名: {local_file}\n"
|
| 439 |
+
f"网盘地址: {share_url}"
|
| 440 |
+
),
|
| 441 |
+
"tasks": task_list
|
| 442 |
+
}, sid)
|
| 443 |
+
else:
|
| 444 |
+
return json_resp({
|
| 445 |
+
"success": True,
|
| 446 |
+
"message": f"任务已完成 (结论: {conclusion})\n但未找到结果文件 (artifact)",
|
| 447 |
+
"tasks": task_list
|
| 448 |
+
}, sid)
|
| 449 |
+
else:
|
| 450 |
+
for task in task_list:
|
| 451 |
+
if task.get("trace_id") == trace_id:
|
| 452 |
+
task["status"] = "失败"
|
| 453 |
+
|
| 454 |
+
result = trigger.get_result(run_id)
|
| 455 |
+
error_info = ""
|
| 456 |
+
if result and isinstance(result, dict) and "error" in result:
|
| 457 |
+
error_info = f"\n错误信息: {result['error']}"
|
| 458 |
+
|
| 459 |
+
return json_resp({
|
| 460 |
+
"success": False,
|
| 461 |
+
"message": f"任务失败\n\n任务 ID: {trace_id}\n结论: {conclusion}{error_info}",
|
| 462 |
+
"tasks": task_list
|
| 463 |
+
}, sid)
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
# ============================================================
|
| 467 |
+
# HTML 模板
|
| 468 |
+
# ============================================================
|
| 469 |
+
HTML_TEMPLATE = r"""<!DOCTYPE html>
|
| 470 |
+
<html lang="zh-CN">
|
| 471 |
+
<head>
|
| 472 |
+
<meta charset="UTF-8">
|
| 473 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 474 |
+
<title>CloudFileRelay|Online Files to Cloud Drive</title>
|
| 475 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 476 |
+
<script>
|
| 477 |
+
tailwind.config = {
|
| 478 |
+
theme: {
|
| 479 |
+
extend: {
|
| 480 |
+
fontFamily: { sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'] }
|
| 481 |
+
}
|
| 482 |
+
}
|
| 483 |
+
}
|
| 484 |
+
</script>
|
| 485 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 486 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 487 |
+
<style>
|
| 488 |
+
body {
|
| 489 |
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
| 490 |
+
background-color: #f8fafc;
|
| 491 |
+
background-image:
|
| 492 |
+
radial-gradient(at 0% 0%, rgba(139,92,246,0.06) 0px, transparent 50%),
|
| 493 |
+
radial-gradient(at 100% 100%, rgba(99,102,241,0.06) 0px, transparent 50%);
|
| 494 |
+
min-height: 100vh;
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
/* 自定义滚动条 */
|
| 498 |
+
::-webkit-scrollbar { width: 5px; height: 5px; }
|
| 499 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 500 |
+
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
|
| 501 |
+
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
| 502 |
+
|
| 503 |
+
/* 开关 */
|
| 504 |
+
.toggle-track {
|
| 505 |
+
width: 38px; height: 20px;
|
| 506 |
+
background: #e2e8f0;
|
| 507 |
+
border-radius: 10px;
|
| 508 |
+
position: relative;
|
| 509 |
+
cursor: pointer;
|
| 510 |
+
transition: background 0.25s ease;
|
| 511 |
+
flex-shrink: 0;
|
| 512 |
+
}
|
| 513 |
+
.toggle-track.active { background: #8b5cf6; }
|
| 514 |
+
.toggle-track::after {
|
| 515 |
+
content: '';
|
| 516 |
+
position: absolute;
|
| 517 |
+
width: 16px; height: 16px;
|
| 518 |
+
background: #fff;
|
| 519 |
+
border-radius: 50%;
|
| 520 |
+
top: 2px; left: 2px;
|
| 521 |
+
transition: transform 0.25s ease;
|
| 522 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
| 523 |
+
}
|
| 524 |
+
.toggle-track.active::after { transform: translateX(18px); }
|
| 525 |
+
|
| 526 |
+
/* 渐入动画 */
|
| 527 |
+
@keyframes fadeInUp {
|
| 528 |
+
from { opacity: 0; transform: translateY(12px); }
|
| 529 |
+
to { opacity: 1; transform: translateY(0); }
|
| 530 |
+
}
|
| 531 |
+
.anim-in { animation: fadeInUp 0.45s ease-out both; }
|
| 532 |
+
.anim-d1 { animation-delay: .06s; }
|
| 533 |
+
.anim-d2 { animation-delay: .12s; }
|
| 534 |
+
.anim-d3 { animation-delay: .18s; }
|
| 535 |
+
|
| 536 |
+
/* toast 滑入 */
|
| 537 |
+
@keyframes toastIn {
|
| 538 |
+
from { opacity: 0; transform: translateX(30px); }
|
| 539 |
+
to { opacity: 1; transform: translateX(0); }
|
| 540 |
+
}
|
| 541 |
+
.toast-in { animation: toastIn 0.3s ease-out both; }
|
| 542 |
+
|
| 543 |
+
/* 按钮加载态 */
|
| 544 |
+
.btn-spin {
|
| 545 |
+
pointer-events: none; opacity: .75;
|
| 546 |
+
}
|
| 547 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 548 |
+
.icon-spin { animation: spin .7s linear infinite; }
|
| 549 |
+
|
| 550 |
+
@keyframes pulse {
|
| 551 |
+
0% { opacity: 1; transform: scale(1); }
|
| 552 |
+
50% { opacity: 0.4; transform: scale(0.8); }
|
| 553 |
+
100% { opacity: 1; transform: scale(1); }
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
@keyframes dots {
|
| 557 |
+
0%, 20% { content: '.'; }
|
| 558 |
+
40% { content: '..'; }
|
| 559 |
+
60% { content: '...'; }
|
| 560 |
+
80%, 100% { content: '....'; }
|
| 561 |
+
}
|
| 562 |
+
.dots::after {
|
| 563 |
+
content: '.';
|
| 564 |
+
display: inline-block;
|
| 565 |
+
width: 24px;
|
| 566 |
+
text-align: left;
|
| 567 |
+
animation: dots 2s infinite;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
/* 卡片 */
|
| 571 |
+
.card {
|
| 572 |
+
background: #fff;
|
| 573 |
+
border-radius: 16px;
|
| 574 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 24px rgba(0,0,0,0.04);
|
| 575 |
+
border: 1px solid rgba(226,232,240,0.8);
|
| 576 |
+
overflow: hidden;
|
| 577 |
+
transition: box-shadow 0.25s;
|
| 578 |
+
}
|
| 579 |
+
.card:hover {
|
| 580 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 8px 32px rgba(0,0,0,0.06);
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
/* 输入框 */
|
| 584 |
+
.input-field {
|
| 585 |
+
width: 100%; padding: 10px 14px;
|
| 586 |
+
border-radius: 10px;
|
| 587 |
+
border: 1px solid #e2e8f0;
|
| 588 |
+
font-size: 14px; color: #334155;
|
| 589 |
+
transition: all .2s;
|
| 590 |
+
outline: none;
|
| 591 |
+
background: #fff;
|
| 592 |
+
}
|
| 593 |
+
.input-field::placeholder { color: #94a3b8; }
|
| 594 |
+
.input-field:focus {
|
| 595 |
+
border-color: #a78bfa;
|
| 596 |
+
box-shadow: 0 0 0 3px rgba(139,92,246,0.1);
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
/* 主按钮 */
|
| 600 |
+
.btn-primary {
|
| 601 |
+
display: inline-flex; align-items: center; gap: 6px;
|
| 602 |
+
padding: 10px 22px;
|
| 603 |
+
background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
|
| 604 |
+
color: #fff; font-weight: 500; font-size: 14px;
|
| 605 |
+
border: none; border-radius: 10px;
|
| 606 |
+
cursor: pointer;
|
| 607 |
+
box-shadow: 0 2px 8px rgba(99,102,241,0.25);
|
| 608 |
+
transition: all .2s;
|
| 609 |
+
}
|
| 610 |
+
.btn-primary:hover {
|
| 611 |
+
transform: translateY(-1px);
|
| 612 |
+
box-shadow: 0 4px 14px rgba(99,102,241,0.35);
|
| 613 |
+
}
|
| 614 |
+
.btn-primary:active { transform: translateY(0); }
|
| 615 |
+
|
| 616 |
+
/* 次按钮 */
|
| 617 |
+
.btn-secondary {
|
| 618 |
+
display: inline-flex; align-items: center; gap: 5px;
|
| 619 |
+
padding: 7px 14px;
|
| 620 |
+
background: #f1f5f9; color: #475569;
|
| 621 |
+
font-weight: 500; font-size: 13px;
|
| 622 |
+
border: 1px solid #e2e8f0; border-radius: 8px;
|
| 623 |
+
cursor: pointer; transition: all .2s;
|
| 624 |
+
}
|
| 625 |
+
.btn-secondary:hover { background: #e2e8f0; }
|
| 626 |
+
|
| 627 |
+
/* 状态徽标 */
|
| 628 |
+
.badge {
|
| 629 |
+
display: inline-flex; align-items: center; gap: 5px;
|
| 630 |
+
padding: 3px 10px;
|
| 631 |
+
border-radius: 20px;
|
| 632 |
+
font-size: 12px; font-weight: 500;
|
| 633 |
+
white-space: nowrap;
|
| 634 |
+
}
|
| 635 |
+
.badge-dot {
|
| 636 |
+
width: 6px; height: 6px; border-radius: 50%;
|
| 637 |
+
flex-shrink: 0;
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
/* 表格 */
|
| 641 |
+
.task-table { width: 100%; border-collapse: collapse; }
|
| 642 |
+
.task-table thead th {
|
| 643 |
+
padding: 10px 14px;
|
| 644 |
+
font-size: 11px; font-weight: 600;
|
| 645 |
+
color: #64748b;
|
| 646 |
+
text-transform: uppercase;
|
| 647 |
+
letter-spacing: 0.05em;
|
| 648 |
+
text-align: left;
|
| 649 |
+
background: #f8fafc;
|
| 650 |
+
border-bottom: 1px solid #e2e8f0;
|
| 651 |
+
}
|
| 652 |
+
.task-table tbody td {
|
| 653 |
+
padding: 12px 14px;
|
| 654 |
+
font-size: 13px;
|
| 655 |
+
color: #334155;
|
| 656 |
+
border-bottom: 1px solid #f1f5f9;
|
| 657 |
+
vertical-align: middle;
|
| 658 |
+
}
|
| 659 |
+
.task-table tbody tr { transition: background .15s; }
|
| 660 |
+
.task-table tbody tr:hover { background: #f8fafc; }
|
| 661 |
+
.task-table tbody tr:last-child td { border-bottom: none; }
|
| 662 |
+
|
| 663 |
+
/* 结果框 */
|
| 664 |
+
.result-box {
|
| 665 |
+
border-radius: 10px; padding: 12px 16px;
|
| 666 |
+
font-size: 13px; line-height: 1.6;
|
| 667 |
+
white-space: pre-wrap; word-break: break-all;
|
| 668 |
+
}
|
| 669 |
+
.result-success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
|
| 670 |
+
.result-error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
|
| 671 |
+
.result-info { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; }
|
| 672 |
+
|
| 673 |
+
/* 链接 */
|
| 674 |
+
a.share-link {
|
| 675 |
+
color: #7c3aed; text-decoration: none;
|
| 676 |
+
transition: color .15s;
|
| 677 |
+
}
|
| 678 |
+
a.share-link:hover { color: #6d28d9; text-decoration: underline; }
|
| 679 |
+
|
| 680 |
+
/* 分页按钮 */
|
| 681 |
+
.btn-page {
|
| 682 |
+
display: inline-flex; align-items: center; justify-content: center;
|
| 683 |
+
min-width: 32px; height: 32px; padding: 0 6px;
|
| 684 |
+
border-radius: 8px; border: 1px solid #e2e8f0;
|
| 685 |
+
background: #fff; color: #64748b;
|
| 686 |
+
font-size: 13px; font-weight: 500;
|
| 687 |
+
cursor: pointer; transition: all .2s;
|
| 688 |
+
}
|
| 689 |
+
.btn-page:hover:not(:disabled) { background: #f1f5f9; border-color: #cbd5e1; color: #334155; }
|
| 690 |
+
.btn-page.active { background: #8b5cf6; border-color: #8b5cf6; color: #fff; }
|
| 691 |
+
.btn-page:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 692 |
+
</style>
|
| 693 |
+
</head>
|
| 694 |
+
|
| 695 |
+
<body>
|
| 696 |
+
<!-- Toast 容器 -->
|
| 697 |
+
<div id="toast-box" style="position:fixed;top:16px;right:16px;z-index:100;display:flex;flex-direction:column;gap:8px;max-width:340px;"></div>
|
| 698 |
+
|
| 699 |
+
<div style="max-width:1200px;margin:0 auto;padding:32px 16px 48px;">
|
| 700 |
+
|
| 701 |
+
<!-- ====== 标题 ====== -->
|
| 702 |
+
<div class="anim-in" style="text-align:center;margin-bottom:36px;">
|
| 703 |
+
<div style="display:inline-flex;align-items:center;justify-content:center;width:56px;height:56px;border-radius:16px;background:linear-gradient(135deg,#8b5cf6,#6366f1);box-shadow:0 4px 16px rgba(99,102,241,0.25);margin-bottom:14px;">
|
| 704 |
+
<span style="font-size:24px;">📦</span>
|
| 705 |
+
</div>
|
| 706 |
+
<h1 style="font-size:22px;font-weight:700;color:#1e293b;margin:0 0 4px;">在线文件云端中转到你的网盘</h1>
|
| 707 |
+
<p style="font-size:14px;color:#94a3b8;margin:0;">输入文件下载链接,自动转存到你的夸克网盘</p>
|
| 708 |
+
</div>
|
| 709 |
+
|
| 710 |
+
<!-- ====== 新建任务 ====== -->
|
| 711 |
+
<div class="card anim-in anim-d1" style="margin-bottom:20px;">
|
| 712 |
+
<div style="padding:14px 20px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:8px;">
|
| 713 |
+
<span style="display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;background:#ede9fe;border-radius:8px;font-size:13px;">📥</span>
|
| 714 |
+
<span style="font-size:14px;font-weight:600;color:#334155;">新建转存任务</span>
|
| 715 |
+
</div>
|
| 716 |
+
<div style="padding:20px;">
|
| 717 |
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
|
| 718 |
+
<div>
|
| 719 |
+
<label style="display:block;font-size:12px;font-weight:500;color:#64748b;margin-bottom:6px;">下载链接</label>
|
| 720 |
+
<input id="url-input" class="input-field" placeholder="https://example.com/file.zip" />
|
| 721 |
+
</div>
|
| 722 |
+
<div>
|
| 723 |
+
<label style="display:block;font-size:12px;font-weight:500;color:#64748b;margin-bottom:6px;">文件名 <span style="color:#94a3b8;font-weight:400;">(自动提取,可修改)</span></label>
|
| 724 |
+
<input id="filename-input" class="input-field" placeholder="自动提取" />
|
| 725 |
+
</div>
|
| 726 |
+
</div>
|
| 727 |
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:4px;">
|
| 728 |
+
<button id="submit-btn" class="btn-primary" onclick="submitTask()">
|
| 729 |
+
<svg id="submit-icon" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
| 730 |
+
<span id="submit-text">开始转存</span>
|
| 731 |
+
</button>
|
| 732 |
+
<span id="task-id-tag" style="font-size:12px;font-family:monospace;color:#a5b4c6;"></span>
|
| 733 |
+
</div>
|
| 734 |
+
<div id="submit-result" class="result-box" style="display:none;margin-top:14px;"></div>
|
| 735 |
+
</div>
|
| 736 |
+
</div>
|
| 737 |
+
|
| 738 |
+
<!-- ====== 任务列表 ====== -->
|
| 739 |
+
<div class="card anim-in anim-d2" style="margin-bottom:20px;">
|
| 740 |
+
<div style="padding:14px 20px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
|
| 741 |
+
<div style="display:flex;align-items:center;gap:8px;">
|
| 742 |
+
<span style="display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;background:#dbeafe;border-radius:8px;font-size:13px;">📋</span>
|
| 743 |
+
<span style="font-size:14px;font-weight:600;color:#334155;">任务列表</span>
|
| 744 |
+
<span id="task-count" style="font-size:11px;color:#94a3b8;font-weight:500;"></span>
|
| 745 |
+
</div>
|
| 746 |
+
<div style="display:flex;align-items:center;gap:12px;">
|
| 747 |
+
<div style="display:flex;align-items:center;gap:6px;">
|
| 748 |
+
<span style="font-size:12px;color:#94a3b8;">自动刷新</span>
|
| 749 |
+
<div id="auto-toggle" class="toggle-track" onclick="toggleAutoRefresh()"></div>
|
| 750 |
+
</div>
|
| 751 |
+
<div style="display:flex;align-items:center;gap:4px;">
|
| 752 |
+
<input id="interval-input" type="number" value="5" min="1" max="120"
|
| 753 |
+
class="input-field" style="width:52px;padding:5px 8px;font-size:12px;text-align:center;"
|
| 754 |
+
onchange="onIntervalChange()">
|
| 755 |
+
<span style="font-size:12px;color:#94a3b8;">秒</span>
|
| 756 |
+
</div>
|
| 757 |
+
<button id="refresh-btn" class="btn-secondary" onclick="refreshTasks()">
|
| 758 |
+
<svg id="refresh-icon" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
| 759 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
| 760 |
+
</svg>
|
| 761 |
+
<span>刷新</span>
|
| 762 |
+
</button>
|
| 763 |
+
<button id="clear-btn" class="btn-secondary" onclick="clearTasks()" style="color:#ef4444; border-color:rgba(239,68,68,0.2);">
|
| 764 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
| 765 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
| 766 |
+
</svg>
|
| 767 |
+
<span>清空</span>
|
| 768 |
+
</button>
|
| 769 |
+
</div>
|
| 770 |
+
</div>
|
| 771 |
+
<div style="padding:16px 20px;">
|
| 772 |
+
<div id="refresh-msg" style="display:none;font-size:12px;color:#94a3b8;margin-bottom:10px;"></div>
|
| 773 |
+
|
| 774 |
+
<!-- 空状态 -->
|
| 775 |
+
<div id="empty-state" style="padding:40px 0;text-align:center;">
|
| 776 |
+
<div style="font-size:36px;margin-bottom:8px;opacity:.7;">📭</div>
|
| 777 |
+
<p style="font-size:13px;color:#94a3b8;margin:0;">暂无转存任务</p>
|
| 778 |
+
<p style="font-size:12px;color:#cbd5e1;margin:4px 0 0;">提交转存任务后,将在此处显示</p>
|
| 779 |
+
</div>
|
| 780 |
+
|
| 781 |
+
<!-- 任务表格 -->
|
| 782 |
+
<div id="table-wrap" style="display:none;overflow-x:auto;border-radius:10px;border:1px solid #e2e8f0;">
|
| 783 |
+
<table class="task-table">
|
| 784 |
+
<thead><tr>
|
| 785 |
+
<th style="min-width:180px;">任务 ID</th>
|
| 786 |
+
<th style="min-width:150px;">文件名</th>
|
| 787 |
+
<th>状态</th>
|
| 788 |
+
<th>原始地址</th>
|
| 789 |
+
<th style="min-width:250px;">网盘地址</th>
|
| 790 |
+
<th style="min-width:120px;">创建时间</th>
|
| 791 |
+
</tr></thead>
|
| 792 |
+
<tbody id="task-tbody"></tbody>
|
| 793 |
+
</table>
|
| 794 |
+
</div>
|
| 795 |
+
|
| 796 |
+
<!-- 分页控制 -->
|
| 797 |
+
<div id="pagination-wrap" style="display:none;margin-top:16px;display:flex;align-items:center;justify-content:center;gap:6px;flex-wrap:wrap;"></div>
|
| 798 |
+
</div>
|
| 799 |
+
</div>
|
| 800 |
+
|
| 801 |
+
<!-- ====== 查询任务 ====== -->
|
| 802 |
+
<div class="card anim-in anim-d3">
|
| 803 |
+
<div style="padding:14px 20px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:8px;">
|
| 804 |
+
<span style="display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;background:#fef3c7;border-radius:8px;font-size:13px;">🔍</span>
|
| 805 |
+
<span style="font-size:14px;font-weight:600;color:#334155;">查询指定任务</span>
|
| 806 |
+
</div>
|
| 807 |
+
<div style="padding:20px;">
|
| 808 |
+
<div style="display:flex;gap:10px;margin-bottom:4px;">
|
| 809 |
+
<input id="query-input" class="input-field" style="flex:1;font-family:monospace;" placeholder="输入任务 ID,如 task_20260208_xxxxxx" />
|
| 810 |
+
<button id="query-btn" class="btn-secondary" onclick="queryTask()" style="white-space:nowrap;">
|
| 811 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path stroke-linecap="round" d="M21 21l-4.35-4.35"/></svg>
|
| 812 |
+
<span id="query-text">查询</span>
|
| 813 |
+
</button>
|
| 814 |
+
</div>
|
| 815 |
+
<div id="query-result" class="result-box" style="display:none;margin-top:14px;"></div>
|
| 816 |
+
</div>
|
| 817 |
+
</div>
|
| 818 |
+
|
| 819 |
+
<!-- 页脚 -->
|
| 820 |
+
<div style="text-align:center;padding:28px 0 0;font-size:12px;color:#cbd5e1;">
|
| 821 |
+
Powered by 小豹
|
| 822 |
+
</div>
|
| 823 |
+
</div>
|
| 824 |
+
|
| 825 |
+
<script>
|
| 826 |
+
/* ============================================================
|
| 827 |
+
State
|
| 828 |
+
============================================================ */
|
| 829 |
+
let autoTimer = null;
|
| 830 |
+
let isSubmitting = false;
|
| 831 |
+
let isRefreshing = false;
|
| 832 |
+
let isQuerying = false;
|
| 833 |
+
let allTasks = [];
|
| 834 |
+
let currentPage = 1;
|
| 835 |
+
const pageSize = 10;
|
| 836 |
+
|
| 837 |
+
/* ============================================================
|
| 838 |
+
Utility
|
| 839 |
+
============================================================ */
|
| 840 |
+
function esc(s) {
|
| 841 |
+
if (!s) return '';
|
| 842 |
+
const d = document.createElement('div');
|
| 843 |
+
d.textContent = s;
|
| 844 |
+
return d.innerHTML;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
function showToast(msg, type) {
|
| 848 |
+
type = type || 'info';
|
| 849 |
+
const box = document.getElementById('toast-box');
|
| 850 |
+
const el = document.createElement('div');
|
| 851 |
+
const colors = {
|
| 852 |
+
success: 'background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;',
|
| 853 |
+
error: 'background:#fef2f2;color:#991b1b;border:1px solid #fecaca;',
|
| 854 |
+
info: 'background:#eff6ff;color:#1e40af;border:1px solid #bfdbfe;',
|
| 855 |
+
warning: 'background:#fffbeb;color:#92400e;border:1px solid #fde68a;',
|
| 856 |
+
};
|
| 857 |
+
el.className = 'toast-in';
|
| 858 |
+
el.style.cssText = 'padding:10px 16px;border-radius:10px;font-size:13px;box-shadow:0 4px 16px rgba(0,0,0,0.08);' + (colors[type] || colors.info);
|
| 859 |
+
el.textContent = msg;
|
| 860 |
+
box.appendChild(el);
|
| 861 |
+
setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateX(30px)'; el.style.transition = 'all .3s'; setTimeout(() => el.remove(), 300); }, 3500);
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
function copyToClipboard(text) {
|
| 865 |
+
if (!text) return;
|
| 866 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 867 |
+
showToast('地址已复制到剪贴板', 'success');
|
| 868 |
+
}).catch(() => {
|
| 869 |
+
// Fallback
|
| 870 |
+
const input = document.createElement('input');
|
| 871 |
+
input.value = text;
|
| 872 |
+
document.body.appendChild(input);
|
| 873 |
+
input.select();
|
| 874 |
+
document.execCommand('copy');
|
| 875 |
+
document.body.removeChild(input);
|
| 876 |
+
showToast('地址已复制到剪贴板', 'success');
|
| 877 |
+
});
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
/* ============================================================
|
| 881 |
+
Status Badges
|
| 882 |
+
============================================================ */
|
| 883 |
+
function statusBadge(s) {
|
| 884 |
+
const m = {
|
| 885 |
+
'未提交': { bg:'#f1f5f9', fg:'#64748b', dot:'#94a3b8', t:'未提交' },
|
| 886 |
+
'正在转存': { bg:'#eff6ff', fg:'#1d4ed8', dot:'#3b82f6', t:'正在转存', pulse:true, dots:true },
|
| 887 |
+
'已转存': { bg:'#f0fdf4', fg:'#15803d', dot:'#22c55e', t:'已完成' },
|
| 888 |
+
'失败': { bg:'#fef2f2', fg:'#b91c1c', dot:'#ef4444', t:'失败' },
|
| 889 |
+
};
|
| 890 |
+
const c = m[s] || m['未提交'];
|
| 891 |
+
const pulseStyle = c.pulse ? 'animation:pulse 1.5s ease-in-out infinite;' : '';
|
| 892 |
+
const dotsHtml = c.dots ? '<span class="dots"></span>' : '';
|
| 893 |
+
return '<span class="badge" style="background:'+c.bg+';color:'+c.fg+';">'
|
| 894 |
+
+ '<span class="badge-dot" style="background:'+c.dot+';'+pulseStyle+'"></span>'
|
| 895 |
+
+ c.t + dotsHtml + '</span>';
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
/* ============================================================
|
| 899 |
+
Render Task Table
|
| 900 |
+
============================================================ */
|
| 901 |
+
function renderTasks(tasks, resetPage = false) {
|
| 902 |
+
allTasks = tasks || [];
|
| 903 |
+
if (resetPage) currentPage = 1;
|
| 904 |
+
displayPage(currentPage);
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
function displayPage(page) {
|
| 908 |
+
const tbody = document.getElementById('task-tbody');
|
| 909 |
+
const empty = document.getElementById('empty-state');
|
| 910 |
+
const wrap = document.getElementById('table-wrap');
|
| 911 |
+
const count = document.getElementById('task-count');
|
| 912 |
+
const pagin = document.getElementById('pagination-wrap');
|
| 913 |
+
|
| 914 |
+
if (!allTasks || !allTasks.length) {
|
| 915 |
+
tbody.innerHTML = '';
|
| 916 |
+
empty.style.display = '';
|
| 917 |
+
wrap.style.display = 'none';
|
| 918 |
+
pagin.style.display = 'none';
|
| 919 |
+
count.textContent = '';
|
| 920 |
+
return;
|
| 921 |
+
}
|
| 922 |
+
|
| 923 |
+
const totalPages = Math.ceil(allTasks.length / pageSize);
|
| 924 |
+
if (page < 1) page = 1;
|
| 925 |
+
if (page > totalPages) page = totalPages;
|
| 926 |
+
currentPage = page;
|
| 927 |
+
|
| 928 |
+
empty.style.display = 'none';
|
| 929 |
+
wrap.style.display = '';
|
| 930 |
+
count.textContent = `共 ${allTasks.length} 条,第 ${currentPage}/${totalPages} 页`;
|
| 931 |
+
|
| 932 |
+
const rows = [...allTasks].reverse();
|
| 933 |
+
const start = (currentPage - 1) * pageSize;
|
| 934 |
+
const end = start + pageSize;
|
| 935 |
+
const pageRows = rows.slice(start, end);
|
| 936 |
+
|
| 937 |
+
tbody.innerHTML = pageRows.map(function(t) {
|
| 938 |
+
const link = t.share_url
|
| 939 |
+
? '<a class="share-link whitespace-nowrap" href="'+esc(t.share_url)+'" target="_blank" rel="noopener">'+esc(t.share_url)+'</a>'
|
| 940 |
+
: '<span style="color:#cbd5e1;">—</span>';
|
| 941 |
+
const originalUrlBtn = t.url
|
| 942 |
+
? '<button class="btn-secondary" style="padding:4px 10px;font-size:11px;" onclick="copyToClipboard(\''+esc(t.url)+'\')">复制地址</button>'
|
| 943 |
+
: '<span style="color:#cbd5e1;">—</span>';
|
| 944 |
+
return '<tr>'
|
| 945 |
+
+ '<td style="font-family:monospace;font-size:12px;color:#64748b;white-space:nowrap;">'+esc(t.trace_id)+'</td>'
|
| 946 |
+
+ '<td style="font-weight:500;white-space:nowrap;">'+esc(t.filename)+'</td>'
|
| 947 |
+
+ '<td>'+statusBadge(t.status)+'</td>'
|
| 948 |
+
+ '<td>'+originalUrlBtn+'</td>'
|
| 949 |
+
+ '<td style="font-size:12px;white-space:nowrap;">'+link+'</td>'
|
| 950 |
+
+ '<td style="font-size:12px;color:#94a3b8;white-space:nowrap;">'+esc(t.created_at)+'</td>'
|
| 951 |
+
+ '</tr>';
|
| 952 |
+
}).join('');
|
| 953 |
+
|
| 954 |
+
renderPaginationControls(totalPages);
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
function renderPaginationControls(totalPages) {
|
| 958 |
+
const pagin = document.getElementById('pagination-wrap');
|
| 959 |
+
if (totalPages <= 1) {
|
| 960 |
+
pagin.style.display = 'none';
|
| 961 |
+
return;
|
| 962 |
+
}
|
| 963 |
+
pagin.style.display = 'flex';
|
| 964 |
+
|
| 965 |
+
let html = '';
|
| 966 |
+
html += `<button class="btn-page" ${currentPage === 1 ? 'disabled' : ''} onclick="displayPage(${currentPage - 1})">上一页</button>`;
|
| 967 |
+
|
| 968 |
+
const maxButtons = 5;
|
| 969 |
+
let startPage = Math.max(1, currentPage - 2);
|
| 970 |
+
let endPage = Math.min(totalPages, startPage + maxButtons - 1);
|
| 971 |
+
if (endPage - startPage < maxButtons - 1) {
|
| 972 |
+
startPage = Math.max(1, endPage - maxButtons + 1);
|
| 973 |
+
}
|
| 974 |
+
|
| 975 |
+
if (startPage > 1) {
|
| 976 |
+
html += `<button class="btn-page" onclick="displayPage(1)">1</button>`;
|
| 977 |
+
if (startPage > 2) html += `<span style="color:#94a3b8;padding:0 4px;">...</span>`;
|
| 978 |
+
}
|
| 979 |
+
|
| 980 |
+
for (let i = startPage; i <= endPage; i++) {
|
| 981 |
+
html += `<button class="btn-page ${i === currentPage ? 'active' : ''}" onclick="displayPage(${i})">${i}</button>`;
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
if (endPage < totalPages) {
|
| 985 |
+
if (endPage < totalPages - 1) html += `<span style="color:#94a3b8;padding:0 4px;">...</span>`;
|
| 986 |
+
html += `<button class="btn-page" onclick="displayPage(${totalPages})">${totalPages}</button>`;
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
html += `<button class="btn-page" ${currentPage === totalPages ? 'disabled' : ''} onclick="displayPage(${currentPage + 1})">下一页</button>`;
|
| 990 |
+
|
| 991 |
+
pagin.innerHTML = html;
|
| 992 |
+
}
|
| 993 |
+
|
| 994 |
+
/* ============================================================
|
| 995 |
+
API Helper
|
| 996 |
+
============================================================ */
|
| 997 |
+
async function api(endpoint, method, body) {
|
| 998 |
+
const opt = { method: method || 'GET', headers: {'Content-Type':'application/json'} };
|
| 999 |
+
if (body) opt.body = JSON.stringify(body);
|
| 1000 |
+
const r = await fetch(endpoint, opt);
|
| 1001 |
+
return await r.json();
|
| 1002 |
+
}
|
| 1003 |
+
|
| 1004 |
+
/* ============================================================
|
| 1005 |
+
URL → 文件名自动提取
|
| 1006 |
+
============================================================ */
|
| 1007 |
+
let urlTimer = null;
|
| 1008 |
+
document.getElementById('url-input').addEventListener('input', function() {
|
| 1009 |
+
clearTimeout(urlTimer);
|
| 1010 |
+
const v = this.value.trim();
|
| 1011 |
+
if (!v) return;
|
| 1012 |
+
urlTimer = setTimeout(async function() {
|
| 1013 |
+
try {
|
| 1014 |
+
const d = await api('/api/extract-filename?url=' + encodeURIComponent(v));
|
| 1015 |
+
if (d.filename) document.getElementById('filename-input').value = d.filename;
|
| 1016 |
+
} catch(e) {}
|
| 1017 |
+
}, 350);
|
| 1018 |
+
});
|
| 1019 |
+
|
| 1020 |
+
/* ============================================================
|
| 1021 |
+
提交任务
|
| 1022 |
+
============================================================ */
|
| 1023 |
+
async function submitTask() {
|
| 1024 |
+
if (isSubmitting) return;
|
| 1025 |
+
const url = document.getElementById('url-input').value.trim();
|
| 1026 |
+
const filename = document.getElementById('filename-input').value.trim();
|
| 1027 |
+
|
| 1028 |
+
if (!url) {
|
| 1029 |
+
showToast('请输入下载链接', 'warning');
|
| 1030 |
+
return;
|
| 1031 |
+
}
|
| 1032 |
+
|
| 1033 |
+
// 初步判断是否是有效的 URL
|
| 1034 |
+
try {
|
| 1035 |
+
const u = new URL(url);
|
| 1036 |
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
| 1037 |
+
throw new Error();
|
| 1038 |
+
}
|
| 1039 |
+
} catch (e) {
|
| 1040 |
+
const box = document.getElementById('submit-result');
|
| 1041 |
+
box.style.display = '';
|
| 1042 |
+
box.className = 'result-box result-error';
|
| 1043 |
+
box.textContent = '转存失败,请输入有效的下载链接地址';
|
| 1044 |
+
showToast('无效的下载链接', 'error');
|
| 1045 |
+
return;
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
isSubmitting = true;
|
| 1049 |
+
|
| 1050 |
+
const btn = document.getElementById('submit-btn');
|
| 1051 |
+
const txt = document.getElementById('submit-text');
|
| 1052 |
+
const ico = document.getElementById('submit-icon');
|
| 1053 |
+
btn.classList.add('btn-spin');
|
| 1054 |
+
ico.classList.add('icon-spin');
|
| 1055 |
+
txt.textContent = '提交中…';
|
| 1056 |
+
|
| 1057 |
+
try {
|
| 1058 |
+
const d = await api('/api/submit', 'POST', { url: url, filename: filename });
|
| 1059 |
+
const box = document.getElementById('submit-result');
|
| 1060 |
+
box.style.display = '';
|
| 1061 |
+
if (d.success) {
|
| 1062 |
+
box.className = 'result-box result-success';
|
| 1063 |
+
box.textContent = d.message;
|
| 1064 |
+
document.getElementById('task-id-tag').textContent = d.task_id || '';
|
| 1065 |
+
renderTasks(d.tasks, true);
|
| 1066 |
+
showToast('转存任务已提交', 'success');
|
| 1067 |
+
// 自动开启刷新
|
| 1068 |
+
const tog = document.getElementById('auto-toggle');
|
| 1069 |
+
if (!tog.classList.contains('active')) { tog.classList.add('active'); startAutoRefresh(); }
|
| 1070 |
+
document.getElementById('url-input').value = '';
|
| 1071 |
+
document.getElementById('filename-input').value = '';
|
| 1072 |
+
} else {
|
| 1073 |
+
box.className = 'result-box result-error';
|
| 1074 |
+
box.textContent = d.message;
|
| 1075 |
+
showToast('提交失败', 'error');
|
| 1076 |
+
}
|
| 1077 |
+
} catch(e) {
|
| 1078 |
+
showToast('网络错误,请重试', 'error');
|
| 1079 |
+
} finally {
|
| 1080 |
+
isSubmitting = false;
|
| 1081 |
+
btn.classList.remove('btn-spin');
|
| 1082 |
+
ico.classList.remove('icon-spin');
|
| 1083 |
+
txt.textContent = '开始转存';
|
| 1084 |
+
}
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
/* ============================================================
|
| 1088 |
+
刷新任务列表
|
| 1089 |
+
============================================================ */
|
| 1090 |
+
async function refreshTasks() {
|
| 1091 |
+
if (isRefreshing) return;
|
| 1092 |
+
isRefreshing = true;
|
| 1093 |
+
const ico = document.getElementById('refresh-icon');
|
| 1094 |
+
ico.classList.add('icon-spin');
|
| 1095 |
+
|
| 1096 |
+
try {
|
| 1097 |
+
const d = await api('/api/refresh', 'POST');
|
| 1098 |
+
renderTasks(d.tasks);
|
| 1099 |
+
const msg = document.getElementById('refresh-msg');
|
| 1100 |
+
msg.style.display = '';
|
| 1101 |
+
|
| 1102 |
+
// 使用正则将时间部分包装在绿色 span 中
|
| 1103 |
+
const formattedMsg = d.message.replace(/\[(\d{2}:\d{2}:\d{2})\]/, '<span class="text-green-600 font-medium">[$1]</span>');
|
| 1104 |
+
msg.innerHTML = formattedMsg;
|
| 1105 |
+
|
| 1106 |
+
if (d.all_done && d.tasks && d.tasks.length) {
|
| 1107 |
+
const tog = document.getElementById('auto-toggle');
|
| 1108 |
+
tog.classList.remove('active');
|
| 1109 |
+
stopAutoRefresh();
|
| 1110 |
+
showToast('所有任务已完成', 'success');
|
| 1111 |
+
}
|
| 1112 |
+
} catch(e) {
|
| 1113 |
+
showToast('刷新失败', 'error');
|
| 1114 |
+
} finally {
|
| 1115 |
+
isRefreshing = false;
|
| 1116 |
+
ico.classList.remove('icon-spin');
|
| 1117 |
+
}
|
| 1118 |
+
}
|
| 1119 |
+
|
| 1120 |
+
/* ============================================================
|
| 1121 |
+
清空任务列表
|
| 1122 |
+
============================================================ */
|
| 1123 |
+
async function clearTasks() {
|
| 1124 |
+
if (!confirm('确定要清空任务列表吗?此操作不可撤销。')) return;
|
| 1125 |
+
|
| 1126 |
+
try {
|
| 1127 |
+
const d = await api('/api/clear', 'POST');
|
| 1128 |
+
if (d.success) {
|
| 1129 |
+
renderTasks([], true);
|
| 1130 |
+
showToast('任务列表已清空', 'success');
|
| 1131 |
+
document.getElementById('refresh-msg').style.display = 'none';
|
| 1132 |
+
stopAutoRefresh();
|
| 1133 |
+
document.getElementById('auto-toggle').classList.remove('active');
|
| 1134 |
+
}
|
| 1135 |
+
} catch(e) {
|
| 1136 |
+
showToast('操作失败', 'error');
|
| 1137 |
+
}
|
| 1138 |
+
}
|
| 1139 |
+
|
| 1140 |
+
/* ============================================================
|
| 1141 |
+
查询指定任务
|
| 1142 |
+
============================================================ */
|
| 1143 |
+
async function queryTask() {
|
| 1144 |
+
if (isQuerying) return;
|
| 1145 |
+
const tid = document.getElementById('query-input').value.trim();
|
| 1146 |
+
if (!tid) { showToast('请输入任务 ID', 'warning'); return; }
|
| 1147 |
+
|
| 1148 |
+
isQuerying = true;
|
| 1149 |
+
const btn = document.getElementById('query-btn');
|
| 1150 |
+
const txt = document.getElementById('query-text');
|
| 1151 |
+
btn.disabled = true;
|
| 1152 |
+
txt.textContent = '查询中…';
|
| 1153 |
+
|
| 1154 |
+
try {
|
| 1155 |
+
const d = await api('/api/query', 'POST', { trace_id: tid });
|
| 1156 |
+
const box = document.getElementById('query-result');
|
| 1157 |
+
box.style.display = '';
|
| 1158 |
+
box.className = 'result-box ' + (d.success !== false ? 'result-info' : 'result-error');
|
| 1159 |
+
box.textContent = d.message;
|
| 1160 |
+
if (d.tasks) renderTasks(d.tasks, true);
|
| 1161 |
+
} catch(e) {
|
| 1162 |
+
showToast('查询失败', 'error');
|
| 1163 |
+
} finally {
|
| 1164 |
+
isQuerying = false;
|
| 1165 |
+
btn.disabled = false;
|
| 1166 |
+
txt.textContent = '查询';
|
| 1167 |
+
}
|
| 1168 |
+
}
|
| 1169 |
+
|
| 1170 |
+
/* ============================================================
|
| 1171 |
+
自动刷新
|
| 1172 |
+
============================================================ */
|
| 1173 |
+
function startAutoRefresh() {
|
| 1174 |
+
stopAutoRefresh();
|
| 1175 |
+
const sec = Math.max(1, parseInt(document.getElementById('interval-input').value) || 5);
|
| 1176 |
+
autoTimer = setInterval(refreshTasks, sec * 1000);
|
| 1177 |
+
}
|
| 1178 |
+
function stopAutoRefresh() {
|
| 1179 |
+
if (autoTimer) { clearInterval(autoTimer); autoTimer = null; }
|
| 1180 |
+
}
|
| 1181 |
+
function toggleAutoRefresh() {
|
| 1182 |
+
const tog = document.getElementById('auto-toggle');
|
| 1183 |
+
tog.classList.toggle('active');
|
| 1184 |
+
if (tog.classList.contains('active')) { startAutoRefresh(); } else { stopAutoRefresh(); }
|
| 1185 |
+
}
|
| 1186 |
+
function onIntervalChange() {
|
| 1187 |
+
const tog = document.getElementById('auto-toggle');
|
| 1188 |
+
if (tog.classList.contains('active')) startAutoRefresh();
|
| 1189 |
+
}
|
| 1190 |
+
|
| 1191 |
+
/* ============================================================
|
| 1192 |
+
快捷键
|
| 1193 |
+
============================================================ */
|
| 1194 |
+
document.getElementById('url-input').addEventListener('keydown', function(e) { if (e.key === 'Enter') submitTask(); });
|
| 1195 |
+
document.getElementById('query-input').addEventListener('keydown', function(e) { if (e.key === 'Enter') queryTask(); });
|
| 1196 |
+
|
| 1197 |
+
/* ============================================================
|
| 1198 |
+
初始化
|
| 1199 |
+
============================================================ */
|
| 1200 |
+
(async function() {
|
| 1201 |
+
try { const d = await api('/api/tasks'); renderTasks(d.tasks); } catch(e) {}
|
| 1202 |
+
})();
|
| 1203 |
+
</script>
|
| 1204 |
+
</body>
|
| 1205 |
+
</html>"""
|
| 1206 |
+
|
| 1207 |
+
|
| 1208 |
+
# ============================================================
|
| 1209 |
+
# 启动
|
| 1210 |
+
# ============================================================
|
| 1211 |
+
if __name__ == "__main__":
|
| 1212 |
+
uvicorn.run("app:app", host="0.0.0.0", port=7860)
|
requirements.txt
CHANGED
|
@@ -1,6 +1,4 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
pandas
|
| 6 |
-
ridgeplot
|
|
|
|
| 1 |
+
fastapi>=0.110.0
|
| 2 |
+
uvicorn>=0.27.0
|
| 3 |
+
requests
|
| 4 |
+
pydantic>=2.0.0
|
|
|
|
|
|
shared.py
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
-
|
| 3 |
-
import pandas as pd
|
| 4 |
-
|
| 5 |
-
app_dir = Path(__file__).parent
|
| 6 |
-
tips = pd.read_csv(app_dir / "tips.csv")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
styles.css
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
:root {
|
| 2 |
-
--bslib-sidebar-main-bg: #f8f8f8;
|
| 3 |
-
}
|
| 4 |
-
|
| 5 |
-
.popover {
|
| 6 |
-
--bs-popover-header-bg: #222;
|
| 7 |
-
--bs-popover-header-color: #fff;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
.popover .btn-close {
|
| 11 |
-
filter: var(--bs-btn-close-white-filter);
|
| 12 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tips.csv
DELETED
|
@@ -1,245 +0,0 @@
|
|
| 1 |
-
total_bill,tip,sex,smoker,day,time,size
|
| 2 |
-
16.99,1.01,Female,No,Sun,Dinner,2
|
| 3 |
-
10.34,1.66,Male,No,Sun,Dinner,3
|
| 4 |
-
21.01,3.5,Male,No,Sun,Dinner,3
|
| 5 |
-
23.68,3.31,Male,No,Sun,Dinner,2
|
| 6 |
-
24.59,3.61,Female,No,Sun,Dinner,4
|
| 7 |
-
25.29,4.71,Male,No,Sun,Dinner,4
|
| 8 |
-
8.77,2.0,Male,No,Sun,Dinner,2
|
| 9 |
-
26.88,3.12,Male,No,Sun,Dinner,4
|
| 10 |
-
15.04,1.96,Male,No,Sun,Dinner,2
|
| 11 |
-
14.78,3.23,Male,No,Sun,Dinner,2
|
| 12 |
-
10.27,1.71,Male,No,Sun,Dinner,2
|
| 13 |
-
35.26,5.0,Female,No,Sun,Dinner,4
|
| 14 |
-
15.42,1.57,Male,No,Sun,Dinner,2
|
| 15 |
-
18.43,3.0,Male,No,Sun,Dinner,4
|
| 16 |
-
14.83,3.02,Female,No,Sun,Dinner,2
|
| 17 |
-
21.58,3.92,Male,No,Sun,Dinner,2
|
| 18 |
-
10.33,1.67,Female,No,Sun,Dinner,3
|
| 19 |
-
16.29,3.71,Male,No,Sun,Dinner,3
|
| 20 |
-
16.97,3.5,Female,No,Sun,Dinner,3
|
| 21 |
-
20.65,3.35,Male,No,Sat,Dinner,3
|
| 22 |
-
17.92,4.08,Male,No,Sat,Dinner,2
|
| 23 |
-
20.29,2.75,Female,No,Sat,Dinner,2
|
| 24 |
-
15.77,2.23,Female,No,Sat,Dinner,2
|
| 25 |
-
39.42,7.58,Male,No,Sat,Dinner,4
|
| 26 |
-
19.82,3.18,Male,No,Sat,Dinner,2
|
| 27 |
-
17.81,2.34,Male,No,Sat,Dinner,4
|
| 28 |
-
13.37,2.0,Male,No,Sat,Dinner,2
|
| 29 |
-
12.69,2.0,Male,No,Sat,Dinner,2
|
| 30 |
-
21.7,4.3,Male,No,Sat,Dinner,2
|
| 31 |
-
19.65,3.0,Female,No,Sat,Dinner,2
|
| 32 |
-
9.55,1.45,Male,No,Sat,Dinner,2
|
| 33 |
-
18.35,2.5,Male,No,Sat,Dinner,4
|
| 34 |
-
15.06,3.0,Female,No,Sat,Dinner,2
|
| 35 |
-
20.69,2.45,Female,No,Sat,Dinner,4
|
| 36 |
-
17.78,3.27,Male,No,Sat,Dinner,2
|
| 37 |
-
24.06,3.6,Male,No,Sat,Dinner,3
|
| 38 |
-
16.31,2.0,Male,No,Sat,Dinner,3
|
| 39 |
-
16.93,3.07,Female,No,Sat,Dinner,3
|
| 40 |
-
18.69,2.31,Male,No,Sat,Dinner,3
|
| 41 |
-
31.27,5.0,Male,No,Sat,Dinner,3
|
| 42 |
-
16.04,2.24,Male,No,Sat,Dinner,3
|
| 43 |
-
17.46,2.54,Male,No,Sun,Dinner,2
|
| 44 |
-
13.94,3.06,Male,No,Sun,Dinner,2
|
| 45 |
-
9.68,1.32,Male,No,Sun,Dinner,2
|
| 46 |
-
30.4,5.6,Male,No,Sun,Dinner,4
|
| 47 |
-
18.29,3.0,Male,No,Sun,Dinner,2
|
| 48 |
-
22.23,5.0,Male,No,Sun,Dinner,2
|
| 49 |
-
32.4,6.0,Male,No,Sun,Dinner,4
|
| 50 |
-
28.55,2.05,Male,No,Sun,Dinner,3
|
| 51 |
-
18.04,3.0,Male,No,Sun,Dinner,2
|
| 52 |
-
12.54,2.5,Male,No,Sun,Dinner,2
|
| 53 |
-
10.29,2.6,Female,No,Sun,Dinner,2
|
| 54 |
-
34.81,5.2,Female,No,Sun,Dinner,4
|
| 55 |
-
9.94,1.56,Male,No,Sun,Dinner,2
|
| 56 |
-
25.56,4.34,Male,No,Sun,Dinner,4
|
| 57 |
-
19.49,3.51,Male,No,Sun,Dinner,2
|
| 58 |
-
38.01,3.0,Male,Yes,Sat,Dinner,4
|
| 59 |
-
26.41,1.5,Female,No,Sat,Dinner,2
|
| 60 |
-
11.24,1.76,Male,Yes,Sat,Dinner,2
|
| 61 |
-
48.27,6.73,Male,No,Sat,Dinner,4
|
| 62 |
-
20.29,3.21,Male,Yes,Sat,Dinner,2
|
| 63 |
-
13.81,2.0,Male,Yes,Sat,Dinner,2
|
| 64 |
-
11.02,1.98,Male,Yes,Sat,Dinner,2
|
| 65 |
-
18.29,3.76,Male,Yes,Sat,Dinner,4
|
| 66 |
-
17.59,2.64,Male,No,Sat,Dinner,3
|
| 67 |
-
20.08,3.15,Male,No,Sat,Dinner,3
|
| 68 |
-
16.45,2.47,Female,No,Sat,Dinner,2
|
| 69 |
-
3.07,1.0,Female,Yes,Sat,Dinner,1
|
| 70 |
-
20.23,2.01,Male,No,Sat,Dinner,2
|
| 71 |
-
15.01,2.09,Male,Yes,Sat,Dinner,2
|
| 72 |
-
12.02,1.97,Male,No,Sat,Dinner,2
|
| 73 |
-
17.07,3.0,Female,No,Sat,Dinner,3
|
| 74 |
-
26.86,3.14,Female,Yes,Sat,Dinner,2
|
| 75 |
-
25.28,5.0,Female,Yes,Sat,Dinner,2
|
| 76 |
-
14.73,2.2,Female,No,Sat,Dinner,2
|
| 77 |
-
10.51,1.25,Male,No,Sat,Dinner,2
|
| 78 |
-
17.92,3.08,Male,Yes,Sat,Dinner,2
|
| 79 |
-
27.2,4.0,Male,No,Thur,Lunch,4
|
| 80 |
-
22.76,3.0,Male,No,Thur,Lunch,2
|
| 81 |
-
17.29,2.71,Male,No,Thur,Lunch,2
|
| 82 |
-
19.44,3.0,Male,Yes,Thur,Lunch,2
|
| 83 |
-
16.66,3.4,Male,No,Thur,Lunch,2
|
| 84 |
-
10.07,1.83,Female,No,Thur,Lunch,1
|
| 85 |
-
32.68,5.0,Male,Yes,Thur,Lunch,2
|
| 86 |
-
15.98,2.03,Male,No,Thur,Lunch,2
|
| 87 |
-
34.83,5.17,Female,No,Thur,Lunch,4
|
| 88 |
-
13.03,2.0,Male,No,Thur,Lunch,2
|
| 89 |
-
18.28,4.0,Male,No,Thur,Lunch,2
|
| 90 |
-
24.71,5.85,Male,No,Thur,Lunch,2
|
| 91 |
-
21.16,3.0,Male,No,Thur,Lunch,2
|
| 92 |
-
28.97,3.0,Male,Yes,Fri,Dinner,2
|
| 93 |
-
22.49,3.5,Male,No,Fri,Dinner,2
|
| 94 |
-
5.75,1.0,Female,Yes,Fri,Dinner,2
|
| 95 |
-
16.32,4.3,Female,Yes,Fri,Dinner,2
|
| 96 |
-
22.75,3.25,Female,No,Fri,Dinner,2
|
| 97 |
-
40.17,4.73,Male,Yes,Fri,Dinner,4
|
| 98 |
-
27.28,4.0,Male,Yes,Fri,Dinner,2
|
| 99 |
-
12.03,1.5,Male,Yes,Fri,Dinner,2
|
| 100 |
-
21.01,3.0,Male,Yes,Fri,Dinner,2
|
| 101 |
-
12.46,1.5,Male,No,Fri,Dinner,2
|
| 102 |
-
11.35,2.5,Female,Yes,Fri,Dinner,2
|
| 103 |
-
15.38,3.0,Female,Yes,Fri,Dinner,2
|
| 104 |
-
44.3,2.5,Female,Yes,Sat,Dinner,3
|
| 105 |
-
22.42,3.48,Female,Yes,Sat,Dinner,2
|
| 106 |
-
20.92,4.08,Female,No,Sat,Dinner,2
|
| 107 |
-
15.36,1.64,Male,Yes,Sat,Dinner,2
|
| 108 |
-
20.49,4.06,Male,Yes,Sat,Dinner,2
|
| 109 |
-
25.21,4.29,Male,Yes,Sat,Dinner,2
|
| 110 |
-
18.24,3.76,Male,No,Sat,Dinner,2
|
| 111 |
-
14.31,4.0,Female,Yes,Sat,Dinner,2
|
| 112 |
-
14.0,3.0,Male,No,Sat,Dinner,2
|
| 113 |
-
7.25,1.0,Female,No,Sat,Dinner,1
|
| 114 |
-
38.07,4.0,Male,No,Sun,Dinner,3
|
| 115 |
-
23.95,2.55,Male,No,Sun,Dinner,2
|
| 116 |
-
25.71,4.0,Female,No,Sun,Dinner,3
|
| 117 |
-
17.31,3.5,Female,No,Sun,Dinner,2
|
| 118 |
-
29.93,5.07,Male,No,Sun,Dinner,4
|
| 119 |
-
10.65,1.5,Female,No,Thur,Lunch,2
|
| 120 |
-
12.43,1.8,Female,No,Thur,Lunch,2
|
| 121 |
-
24.08,2.92,Female,No,Thur,Lunch,4
|
| 122 |
-
11.69,2.31,Male,No,Thur,Lunch,2
|
| 123 |
-
13.42,1.68,Female,No,Thur,Lunch,2
|
| 124 |
-
14.26,2.5,Male,No,Thur,Lunch,2
|
| 125 |
-
15.95,2.0,Male,No,Thur,Lunch,2
|
| 126 |
-
12.48,2.52,Female,No,Thur,Lunch,2
|
| 127 |
-
29.8,4.2,Female,No,Thur,Lunch,6
|
| 128 |
-
8.52,1.48,Male,No,Thur,Lunch,2
|
| 129 |
-
14.52,2.0,Female,No,Thur,Lunch,2
|
| 130 |
-
11.38,2.0,Female,No,Thur,Lunch,2
|
| 131 |
-
22.82,2.18,Male,No,Thur,Lunch,3
|
| 132 |
-
19.08,1.5,Male,No,Thur,Lunch,2
|
| 133 |
-
20.27,2.83,Female,No,Thur,Lunch,2
|
| 134 |
-
11.17,1.5,Female,No,Thur,Lunch,2
|
| 135 |
-
12.26,2.0,Female,No,Thur,Lunch,2
|
| 136 |
-
18.26,3.25,Female,No,Thur,Lunch,2
|
| 137 |
-
8.51,1.25,Female,No,Thur,Lunch,2
|
| 138 |
-
10.33,2.0,Female,No,Thur,Lunch,2
|
| 139 |
-
14.15,2.0,Female,No,Thur,Lunch,2
|
| 140 |
-
16.0,2.0,Male,Yes,Thur,Lunch,2
|
| 141 |
-
13.16,2.75,Female,No,Thur,Lunch,2
|
| 142 |
-
17.47,3.5,Female,No,Thur,Lunch,2
|
| 143 |
-
34.3,6.7,Male,No,Thur,Lunch,6
|
| 144 |
-
41.19,5.0,Male,No,Thur,Lunch,5
|
| 145 |
-
27.05,5.0,Female,No,Thur,Lunch,6
|
| 146 |
-
16.43,2.3,Female,No,Thur,Lunch,2
|
| 147 |
-
8.35,1.5,Female,No,Thur,Lunch,2
|
| 148 |
-
18.64,1.36,Female,No,Thur,Lunch,3
|
| 149 |
-
11.87,1.63,Female,No,Thur,Lunch,2
|
| 150 |
-
9.78,1.73,Male,No,Thur,Lunch,2
|
| 151 |
-
7.51,2.0,Male,No,Thur,Lunch,2
|
| 152 |
-
14.07,2.5,Male,No,Sun,Dinner,2
|
| 153 |
-
13.13,2.0,Male,No,Sun,Dinner,2
|
| 154 |
-
17.26,2.74,Male,No,Sun,Dinner,3
|
| 155 |
-
24.55,2.0,Male,No,Sun,Dinner,4
|
| 156 |
-
19.77,2.0,Male,No,Sun,Dinner,4
|
| 157 |
-
29.85,5.14,Female,No,Sun,Dinner,5
|
| 158 |
-
48.17,5.0,Male,No,Sun,Dinner,6
|
| 159 |
-
25.0,3.75,Female,No,Sun,Dinner,4
|
| 160 |
-
13.39,2.61,Female,No,Sun,Dinner,2
|
| 161 |
-
16.49,2.0,Male,No,Sun,Dinner,4
|
| 162 |
-
21.5,3.5,Male,No,Sun,Dinner,4
|
| 163 |
-
12.66,2.5,Male,No,Sun,Dinner,2
|
| 164 |
-
16.21,2.0,Female,No,Sun,Dinner,3
|
| 165 |
-
13.81,2.0,Male,No,Sun,Dinner,2
|
| 166 |
-
17.51,3.0,Female,Yes,Sun,Dinner,2
|
| 167 |
-
24.52,3.48,Male,No,Sun,Dinner,3
|
| 168 |
-
20.76,2.24,Male,No,Sun,Dinner,2
|
| 169 |
-
31.71,4.5,Male,No,Sun,Dinner,4
|
| 170 |
-
10.59,1.61,Female,Yes,Sat,Dinner,2
|
| 171 |
-
10.63,2.0,Female,Yes,Sat,Dinner,2
|
| 172 |
-
50.81,10.0,Male,Yes,Sat,Dinner,3
|
| 173 |
-
15.81,3.16,Male,Yes,Sat,Dinner,2
|
| 174 |
-
7.25,5.15,Male,Yes,Sun,Dinner,2
|
| 175 |
-
31.85,3.18,Male,Yes,Sun,Dinner,2
|
| 176 |
-
16.82,4.0,Male,Yes,Sun,Dinner,2
|
| 177 |
-
32.9,3.11,Male,Yes,Sun,Dinner,2
|
| 178 |
-
17.89,2.0,Male,Yes,Sun,Dinner,2
|
| 179 |
-
14.48,2.0,Male,Yes,Sun,Dinner,2
|
| 180 |
-
9.6,4.0,Female,Yes,Sun,Dinner,2
|
| 181 |
-
34.63,3.55,Male,Yes,Sun,Dinner,2
|
| 182 |
-
34.65,3.68,Male,Yes,Sun,Dinner,4
|
| 183 |
-
23.33,5.65,Male,Yes,Sun,Dinner,2
|
| 184 |
-
45.35,3.5,Male,Yes,Sun,Dinner,3
|
| 185 |
-
23.17,6.5,Male,Yes,Sun,Dinner,4
|
| 186 |
-
40.55,3.0,Male,Yes,Sun,Dinner,2
|
| 187 |
-
20.69,5.0,Male,No,Sun,Dinner,5
|
| 188 |
-
20.9,3.5,Female,Yes,Sun,Dinner,3
|
| 189 |
-
30.46,2.0,Male,Yes,Sun,Dinner,5
|
| 190 |
-
18.15,3.5,Female,Yes,Sun,Dinner,3
|
| 191 |
-
23.1,4.0,Male,Yes,Sun,Dinner,3
|
| 192 |
-
15.69,1.5,Male,Yes,Sun,Dinner,2
|
| 193 |
-
19.81,4.19,Female,Yes,Thur,Lunch,2
|
| 194 |
-
28.44,2.56,Male,Yes,Thur,Lunch,2
|
| 195 |
-
15.48,2.02,Male,Yes,Thur,Lunch,2
|
| 196 |
-
16.58,4.0,Male,Yes,Thur,Lunch,2
|
| 197 |
-
7.56,1.44,Male,No,Thur,Lunch,2
|
| 198 |
-
10.34,2.0,Male,Yes,Thur,Lunch,2
|
| 199 |
-
43.11,5.0,Female,Yes,Thur,Lunch,4
|
| 200 |
-
13.0,2.0,Female,Yes,Thur,Lunch,2
|
| 201 |
-
13.51,2.0,Male,Yes,Thur,Lunch,2
|
| 202 |
-
18.71,4.0,Male,Yes,Thur,Lunch,3
|
| 203 |
-
12.74,2.01,Female,Yes,Thur,Lunch,2
|
| 204 |
-
13.0,2.0,Female,Yes,Thur,Lunch,2
|
| 205 |
-
16.4,2.5,Female,Yes,Thur,Lunch,2
|
| 206 |
-
20.53,4.0,Male,Yes,Thur,Lunch,4
|
| 207 |
-
16.47,3.23,Female,Yes,Thur,Lunch,3
|
| 208 |
-
26.59,3.41,Male,Yes,Sat,Dinner,3
|
| 209 |
-
38.73,3.0,Male,Yes,Sat,Dinner,4
|
| 210 |
-
24.27,2.03,Male,Yes,Sat,Dinner,2
|
| 211 |
-
12.76,2.23,Female,Yes,Sat,Dinner,2
|
| 212 |
-
30.06,2.0,Male,Yes,Sat,Dinner,3
|
| 213 |
-
25.89,5.16,Male,Yes,Sat,Dinner,4
|
| 214 |
-
48.33,9.0,Male,No,Sat,Dinner,4
|
| 215 |
-
13.27,2.5,Female,Yes,Sat,Dinner,2
|
| 216 |
-
28.17,6.5,Female,Yes,Sat,Dinner,3
|
| 217 |
-
12.9,1.1,Female,Yes,Sat,Dinner,2
|
| 218 |
-
28.15,3.0,Male,Yes,Sat,Dinner,5
|
| 219 |
-
11.59,1.5,Male,Yes,Sat,Dinner,2
|
| 220 |
-
7.74,1.44,Male,Yes,Sat,Dinner,2
|
| 221 |
-
30.14,3.09,Female,Yes,Sat,Dinner,4
|
| 222 |
-
12.16,2.2,Male,Yes,Fri,Lunch,2
|
| 223 |
-
13.42,3.48,Female,Yes,Fri,Lunch,2
|
| 224 |
-
8.58,1.92,Male,Yes,Fri,Lunch,1
|
| 225 |
-
15.98,3.0,Female,No,Fri,Lunch,3
|
| 226 |
-
13.42,1.58,Male,Yes,Fri,Lunch,2
|
| 227 |
-
16.27,2.5,Female,Yes,Fri,Lunch,2
|
| 228 |
-
10.09,2.0,Female,Yes,Fri,Lunch,2
|
| 229 |
-
20.45,3.0,Male,No,Sat,Dinner,4
|
| 230 |
-
13.28,2.72,Male,No,Sat,Dinner,2
|
| 231 |
-
22.12,2.88,Female,Yes,Sat,Dinner,2
|
| 232 |
-
24.01,2.0,Male,Yes,Sat,Dinner,4
|
| 233 |
-
15.69,3.0,Male,Yes,Sat,Dinner,3
|
| 234 |
-
11.61,3.39,Male,No,Sat,Dinner,2
|
| 235 |
-
10.77,1.47,Male,No,Sat,Dinner,2
|
| 236 |
-
15.53,3.0,Male,Yes,Sat,Dinner,2
|
| 237 |
-
10.07,1.25,Male,No,Sat,Dinner,2
|
| 238 |
-
12.6,1.0,Male,Yes,Sat,Dinner,2
|
| 239 |
-
32.83,1.17,Male,Yes,Sat,Dinner,2
|
| 240 |
-
35.83,4.67,Female,No,Sat,Dinner,3
|
| 241 |
-
29.03,5.92,Male,No,Sat,Dinner,3
|
| 242 |
-
27.18,2.0,Female,Yes,Sat,Dinner,2
|
| 243 |
-
22.67,2.0,Male,Yes,Sat,Dinner,2
|
| 244 |
-
17.82,1.75,Male,No,Sat,Dinner,2
|
| 245 |
-
18.78,3.0,Female,No,Thur,Dinner,2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|