code:
---
title: Garden with Clojurescript
date: 2016-02-21
footnotes:
  - The real power here is that you get access to Clojure's full battery of rich data transformation capabilities directly available to you while writing styles! This beats out Sass loops and logic 10-to-1 from my perspective -- assuming your stack is already Clojure(script)-based.
  - For prototyping the style tag approach is nice, but if you can save time and processing power on the client's end for a production site, you should.
---

[Garden](https://github.com/noprompt/garden) is a neat Clojure *and* Clojurescript library that lets you render CSS by writing your styles in a simple Clojure(script) DSL1.

Garden stylesheets are written ambivalent to whether Clojure (JVM host) or Clojurescript (Javascript host) is being used for their compilation -- as such the below snippet will render the same in both contexts.

```clojure
(garden/css [:h1 {:color "blue"}])
; "h1 { color: blue; }"
```

Being available in both Clojure and Clojurescript, I've found for rapid prototyping, Garden via Clojurescript is simple and accesible. For quickly getting moving in an existing Clojurescript app, I've found it helpful to just output my styles returned from Garden into a simple style tag:

```clojure
(defn generate-and-inject-style-tag
  "Injects a style tag with the id 'injected-css' into the page's head tag
   Returns generated style tag"
  []
  (let [ page-head (.-head js/document)
         style-tag (.createElement js/document "style")]
       (.setAttribute style-tag "id" "injected-css")
       (.appendChild page-head style-tag)))

(defn update-page-css
  "Updates #injected-css with provided argument (should be some CSS string
   -- e.g. output from garden's css fn) If page does not have #injected-css then
   will create it via call to generate-and-inject-style-tag"
  [input-css]
  (let [ style-tag-selector "#injected-css"
         style-tag-query (.querySelector js/document style-tag-selector)
         style-tag (if (nil? style-tag-query)
                       (generate-and-inject-style-tag)
                       style-tag-query)]
       (aset style-tag "innerHTML" input-css)))

; Usage
; (Assumes you've required garden in your namespace)
; (Optionally throw this in your boot-reload / figwheel reload CB)
(update-page-css
	(garden/css [:h1 {:color "blue"}]))
```

Although, I'll likely move my Garden styles to Clojure eventually2...  for prototyping purposes, in combination with [boot-reload](https://github.com/adzerk-oss/boot-reload), the above defns get you on-the-fly style updating similar to [figwheel](https://github.com/bhauman/lein-figwheel)'s CSS reloading capabilities but without the extra file IO overhead. And you get to keep everything in Clojurescript!


code:
---
title: Monome via Serial and Go
date: 2017-07-01
footnotes:
  - The example Go code was tested on a non-varibright Monome Walnut 256 model.
---

If you have a [Monome](https://en.wikipedia.org/wiki/Monome), you're probably familiar with [serialosc](https://github.com/monome/serialosc) which does what its name implies - converts Monome's serial to OSC.  However, serialosc, isn't the only way to talk to these devices. You can talk serial directly to monomes fairly simply as well.

Below is a contrived example1 using Go:

```go
package main
import (
	"fmt"
	"io"
	"log"
	"time"
	"github.com/jacobsa/go-serial/serial"
)

func setLed(port io.ReadWriteCloser, position int, on bool) {
	var a, b byte
	if on {
		a, b = 1, 0x11
	} else {
		a, b = 2, 0x10
	}
	port.Write([]byte{(a << 4) + b, byte(position)})
}
func main() {
	port, err := serial.Open(serial.OpenOptions{
		PortName:        "/dev/ttyUSB0",
		BaudRate:        115200,
		DataBits:        8,
		StopBits:        1,
		MinimumReadSize: 1,
	})
	defer port.Close()
	if err != nil {
		log.Fatalf("Monome not found: %v", err)
	}
	// Read example, just print information
	go func() {
		readBytes := make([]byte, 2)
		for {
			port.Read(readBytes)
			fmt.Printf("%v\n", readBytes)
		}
	}()
	// Write example, cycle all leds off/on
	s := false
	for {
		for x := 0; x < 256; x++ {
			setLed(port, x, s)
		}
		s = !s
		time.Sleep(100 * time.Millisecond)
	}
}
```

In the above snippet you get both reading and writing directly from/to the Monome's serial port. The [serial specification](http://monome.org/docs/serial.txt) is well laid out and working directly with serial rather than using serialosc might be a good option if you don't need all the fancy features (such as Bonjour or multi-device support) of serialosc or just want something a little more low-level.

license: wtfpl


code:
---
title: Trackman Marble with xset
date: 2017-07-29
footnotes:
  - Make sure to install xset and libinput
---

The [Trackman Marble](https://www.logitech.com/en-us/product/trackman-marble) is a great mouse I've been using for over a decade now. By default on linux / within X, scrolling funcionality doesnt not work. Instead, by default the small mouse buttons act as browser forward/back. Because, I've had to set this up so many times with Xorg's `/etc/X11/Xorg.conf`, and that feels very 2005 at this point, I've moved over to a small userland script.

Using [xset](https://www.x.org/archive/X11R7.5/doc/man/man1/xset.1.html) w/ [libinput](https://www.freedesktop.org/wiki/Software/libinput/) I've found is the most pain-free solution to set the mouse to your liking. For my purposes, really all I wanted was to hold down the left small button in order to access a scroll modifier. So you hold the button and then you can use the trackball for scrolling. Well, that's enough blathering, here's the script I use1:

```bash
#!/usr/bin/env bash
xinput set-prop "Logitech USB Trackball" "libinput Scroll Method Enabled" "0" "0" "1"
xinput set-prop "Logitech USB Trackball" "libinput Horizontal Scroll Enabled" "0"
xinput set-prop "Logitech USB Trackball" "libinput Drag Lock Buttons" "8"
xinput set-prop "Logitech USB Trackball" "libinput Button Scrolling Button" "8"
```


code:
---
title: Host-Specific i3 Config Variables
date: 2017-08-04
footnotes:
  - i3 reads the Xresources properties currently applied; so xrdb ~/.Xresources after making changes to .Xresources
  - Link to i3's docs on reading Xresouce properties into variables
---

i3 doesn't let you define conditionals for per-host settings.  So say you want to make your font larger *only* on your machine that has a 4k display (using 1 `.i3/config`) that becomes difficult.. However, as you may or may not know, you *can* define per-host conditionals in your `.Xresources` file using an `#ifdef`. Further, as it turns out; i3 now supports *reading* Xresources properties. As such, sharing an i3 config between hosts applying slightly different variables on each host for specific items is now trivial.

In your `.Xresources` put something like:

```bash
urxvt.font: xft:Monospace:pixelsize=10
#ifdef SRVR_somehostname
  urxvt.font: xft:Monospace:pixelsize=20
  *i3font: pango:Monospace 20
#endif
```

and then in your `.i3/config`, use i3's new `set_from_resource` command like so:

```bash
set_from_resource $i3font i3wm.i3font pango:Monospace 10font $i3font
font $i3font
```

And that's it. By default on normal hosts, your font will be size 10; and on your `somehostname` host, your font size will be 20 in both urxvt and i3. Rinse and repeat for other variables you might want to change.12


code:
---
title: Using Vis Structural Regexes
date: 2018-04-09
footnotes:
- For more comprehensive documentation check out the vis man page and also Rob Pike's original paper on structural regexes.
---
The [vis editor](https://github.com/martanne/vis) is a great drop-in vim replacement that supports a feature known as structural regexes. The idea of strucutral regexes is best illustrated by example:

```md
one something
two something
unrelated
three something
another
four something
```

In the above snippet, to obtain the selection of lines only containg the word "something", you could use the **x** command as `:x/.*something.*/` on the list, to obtain the selection:

```md
one something
two something
three something
four something
```

Now further, say you only want to match only the remaining lines for `three something`.. You have two options. Either use **x** command again to re-*select* (`:x/.*three.*/`). Or the more interesting alternative is to use the **g** command (`:g/three/`) to *conditionally filter* selections matching the regex. Both results will give you:

```md
three something
```

The **g** command can be thought of as an if-regex-filter-loop and **x** as reselect-regex-loop. Also at your disposal are the **v** and **y** commands which are the inverse of **g** and **x**.


code:
---
title: Linux Kernel Fragments
date: 2019-05-05
footnotes: []
---
I've switched to compiling my own kernel in the past few months and I think an option for handling kernel configuration that doesn't get enough praise is the kernel fragments `merge_config.sh` script included in the kernel sourcetree. This script let's you merge together two seperate sets of kernel configurations.

The reason why that's helpful is because often you don't want to sit there and manually go through every option to create your own kernel configuration but you'd rather just flip a few flags on.

For me, what I like to do is generate the `defconfig` and then use the kernel fragments script to merge a few custom options ontop to generate kernel configuration. This is essentially the layman (or lazyman's) customized kernel. You could achieve similar to results with gentoo's `genkernel` features; but using just the defconfig gives you a substantially smaller build and magnitudes faster build time.

My kernel build script looks something like:

```
#!/usr/bin/env sh
# Script assumes a file called config_overlay with one config option per-line exists
PAR=4

echo "Building the kernel"
time make defconfig # E.g. generates a new .config (or overwrites)
time ./scripts/kconfig/merge_config.sh .config config_overlay
time make -j$PAR
time make -j$PAR install
time make -j$PAR modules_install
grub-mkconfig -o /boot/grub/grub.cfg
```

Defconfig gives you some usable defaults and whatever flags you need you can just throw in a config file (which is what you actually keep track of / care about - instead moving around and tracking the multi-thousand line `.config`).


code:
---
title: Minimal Tinyproxy Config
date: 2019-05-18
footnotes:
  - E.g. Common usecases for local development for me are like avoiding CORS or adding in extra HTTP headers
---

Tinyproxy may be traditionally thought of as more of a server-side tool, but it's also great for local development where you might use MITM proxy or ModHeaders etc1.

Here's a minimal (or tiny...) config for `tinyproxy` to serve as a starting point:

```
User tinyproxy
Group tinyproxy
Port 8888
LogLevel Info
DisableViaHeader Yes
MaxClients 100
StartServers 1

ReverseOnly Yes
ReversePath "/" "http://some-host.com"
ReverseBaseURL "http://localhost:8888"
upstream http some-host:80
AddHeader "Authorization" "Basic add-auth-header-here-if-needed"
```

In this example were just proxying all requests on `localhost:8888` to `some-host.com`.

So if the API your trying to hit is `some-host.com/api/endpoint` instead you could hit `localhost:8888/api/endpoint`. Also we're adding in the Authorization header.

Save the above snippet as `tiny.conf` and use it like:

```
tinyproxy -dc tiny.conf
```


code:
---
title: 'Surf externalpipe patch: browsing via dmenu'
date: 2019-05-28
footnotes:
  - 'Basically just a lot of awk and xmlint - see script the here, but pretty cool in my view that "clicking" a link is piping through awk and coreutils :)'
  - 'Two other patches are worth mentioning for combining externalpipe/surf_linkselect.sh with to make the workflow even more efficient: (1) the surf modal patch can enable single keypress bindings (e.g. press u for example instead of C-u) and the dmenu instant patch can eliminate the need to press enter and just take you to the page once you have narrowed down the results to one item.'
  - 'So far, additionally I have also got find functionality with full search completion working (e.g. dmenu for search now shows / completes fragments of each word) in addition to image extraction in a dmenu list, check out my dot-files repo if interested.'
---

Recently, I've taken the time to add a new feature to my favorite web-browser, suckless's [surf](http://surf.suckless.org). My patch is called **externalpipe**, and similar to the [patch for st with the same name](http://st.suckless.org/patches/externalpipe), the [surf externalpipe patch](http://surf.suckless.org/patches/externalpipe) allows you to arbitrarily pipe the *sourcecode* / raw HTML  of the current page you're viewing to an external program.

Combine this with [a shell script](http://surf.suckless.org/patches/externalpipe/surf_linkselect.sh)1 I made to extract all the page's links from that piped content and you get a browsing workflow that looks something like this:

In effect the externalpipe patch and my script in combination enables fully keyboard-based web-browsing via dmenu in surf. Instead of clicking links on a page, you press a keybinding and bam you get a dmenu popup with all links on the page. Type any part of the link and you're taken there2.

I've been playing around with this setup for the past few weeks and for general browsing its really efficient and my urge to reach for the mouse is gone. Also I can envision the externalpipe patch enabling some other new creative uses for surf hacking3.


code:
---
title: 'Simple LPEG for Lisp (vis editor)'
date: 2019-06-01
---

Getting back into clojure recently has made me realize how nice lisp is in that is has almost no "syntax". Everything on a page of clojure code is basically just function calls; so really there's just two kinds of *things*: function names and arguments. Syntax highlighting for lisp as such I think should reflect that simplicity.

So I decided I'd write my own (minimal) syntax highlighter for my preferred editor [vis](https://github.com/martanne/vis) in LPEG. What I wanted in my syntax highlighter for lisp to highlight was simple: (1) comments, (2) function names (e.g. the first parameter in lists), and (3) the first function argument (e.g. all function second argument in case of defn etc.)

Here's the final lpeg highlighter code (e.g. add to `~/.config/vis/themes/mlisp.clj` and then `set syntax=mlisp`):

```lua
local l = require('lexer')
local token, word_match = l.token, l.word_match
local P, R, S, C = lpeg.P, lpeg.R, lpeg.S, lpeg.C
local M = {_NAME = 'lisp'}

local line_comment = ';' * l.nonnewline^0
local block_comment = '#_(' * (l.any - ')')^0 * P(')')
local firstargchars = l.any - ' ' - ')' - '(';
local secondargchars = l.any - ' ' - ')' - '(' - '[' - ']' - '"';

local fn_call_arg_one = token(
  l.KEYWORD, lpeg.B('(') * (firstargchars)^1
);
local fn_call_arg_two  = token(
  l.TYPE, (' ' * secondargchars^1)^-1
);
local comment = token(
  l.COMMENT, line_comment + block_comment
);

M._rules = {
  {'fn_call', fn_call_arg_one * fn_call_arg_two },
  {'comment', comment}
};

return M;
```


code:
---
archived: true
title: Cream Minitouch
date: 2014-01-13
poster: /assets/prj_img/cream_minitouch/poster.jpg
description: An ALPs Cream keyswitch mod
links:
    'Alps Keyswitches': http://deskthority.net/wiki/Alps_SKCM_Cream_Damped
    'Siig Minitouch': http://deskthority.net/wiki/SIIG_MiniTouch
category: physical
sort_index: 1001
---
The SIIG MiniTouch is a 75% mechnical keyboard produced circa 1991. At the time of manufacture, the MiniTouch was outfitted with either [clicky white](http://deskthority.net/wiki/Alps_SKCM_White) or [montery blue ](http://deskthority.net/wiki/SMK_Alps_mount) alp switches.  The particular model I had was an order magnitude louder than any keyboard I'd ever owned.

So, I modded some [cream alps switches](http://deskthority.net/wiki/Alps_SKCM_Cream_Damped) from an old AE II keyboard to be very light and less audible. Each switch had its internal curled springs end cut and dampeners removed for cleaner feel.

![Alps switches dissambled](/assets/prj_img/cream_minitouch/switches-dissembled-2.jpg)

A stock dissembled Cream Alps switch on the left, and my un-dampened low-resistance cream switch on the right.

After preparing my modded each switch, I de-soldered all the switches from my Minitouch and soldered in my new modded cream switches.  Re-assembled and swapped on the AEII PBT keycaps.

![Stock Minitouch and modded Minitouch](/assets/prj_img/cream_minitouch/side-by-side.jpg)

The stock Minitouch on the left, my modded Minitouch on the right.

![Jumped circuts](/assets/prj_img/cream_minitouch/jumped-circutboard.jpg)

Unfortunately due to my lack of pro-soldering skills, I had to jump a few of the switches.

![Modded Minitouch](/assets/prj_img/cream_minitouch/1.jpg)

Not all the keycaps from the Apple Extended II keyboard fit! So I use the original Enter, Shift, Spacebar, and tab key. Also The Apple F-keys fit on sideways!


code:
---
archived: true
title: Asciiw
date: 2014-02-12
description: ASCII Weather Report
status: 'production'
category: web
sort_index: 4
links:
    'Site': http://asciiw.userbound.com
    'Source' : http://github.com/mil/asciiw.userbound.com
    'Animation Frames' : https://github.com/mil/asciiw.userbound.com/blob/master/js/frames.js
---

[Asciiw](http://asciiw.userbound.com) is a little weather site I made using the [wunderground api](http://www.wunderground.com/weather/api/) which shows the basic weather status for your location along with some nice handcrafted ASCII animations.

Each animation is custom drawn in ASCII and cycled through with javascript. For example, here's the animation for rain!

<script src="/assets/prj_demo/asciiw/js/frames.js"></script>
<script>
window.onload = function() {
  var wrap = document.getElementById("ascii-frames");
  if (wrap == null) { return; }

  var condition = 'raining';
  window.slide_count = 0;
  setInterval(function() {
    var frame = slides[condition]['frames'][window.slide_count].join("\n");
    window.slide_count =
      (window.slide_count == slides[condition]['frames'].length - 1) ?
        0 : window.slide_count + 1;

    wrap.innerHTML = frame;
 }, slides[condition]['interval']);
}
</script>

<pre class='ascii-frames' id="ascii-frames">
               ,--.,
      ,--,--. (     ),   ,--.,
     (       (,___,__)  (     ),
    (,___,____,)       (,___,__)
                 |
      |                   |
            |
                |      |
          |

             |      |
</pre>

By default [asciiw](http://asciiw.userbound.com) uses my zip code in Madison, WI but can be switched to any zip code adding `/?zipcode` to the URL.

For example, to get the weather for Blacksburg, Virginia zipcode 24060 simply visit [asciiw.userbound.com/?24060](http://asciiw.userbound.com/?24060).


code:
---
archived: false
title: Foo Window Manager
date: 2014-02-12
description: Minimal IPC/DSL-based Zooming Wm
category: linux
status: 'experimental'
links:
    'Source' : http://github.com/mil/foo-wm
    'Foo DSL Docs' : https://github.com/mil/foo-wm/blob/master/FOO-DSL.md
    'Aur PKGBUILD' : https://aur.archlinux.org/packages/foo-wm-git/
sort_index: 100
footnotes:
    - 'no-wm is an amazing proof-of-concept.'
    - "Bastein stated on the herbluftwm mailing list that Bspwm's server/client IPC skeleton was originally extracted from foo-wm! Although, Bspwm has evolved much since then, it's nice to know foo-wm served as an inspiration."
---

[Foo Wm](http://github.com/mil/foo-wm) was an idealistic & minimalistic wm for X11 which I built over the course of a summer while doing [Hacker School](http://hackerschool.com).  The architecture of the wm was simple, involving only two components:

- A tree data structure to hold windows **and**
- A DSL to manipulate & traverse the window tree

In `foo-wm` 'tiling' and 'floating' modals of organization familiar in WIMP were just setting values of containers.  'Workspaces' were just the immediate containers of the root node.  And 'full-screening' a window meant simply issuing a `zoom` DSL command with a high positive delta.

Meta: at work, using `foo-wm` to edit `foo-wm`'s code.

Workspace's didn't exist within `foo-wm`, because given the tree structure they were easily emulated using the `zoom` command.

The [Foo DSL](https://github.com/mil/foo-wm/blob/master/FOO-DSL.md), accessible via a socket, was the only way to interact with `foo-wm` itself. In fact, `foo-wm` didn't even provide a way to bind keys to wm actions, because [xbindkeys](https://github.com/mil/foo-wm/blob/master/config/.xbindkeysrc) could and still can do that anyway.

All commands in the [Foo DSL](https://github.com/mil/foo-wm/blob/master/FOO-DSL.md) operated on two pointer nodes within the tree. The active node controlled the node currently through-putting mouse and keyboard events; and the view node represented the node that the screen was currently displaying. The [full DSL](https://github.com/mil/foo-wm/blob/master/FOO-DSL.md) was as follows:

|Command   |Targets Node | Purpose |
|:---------|:------------|:--------|
|zoom      | `view node` | Zooms screen through tree. |
|containerize | `focus node` | Wraps current node in a new parent node. |
|focus | `focus node` | Moves focus pointer to brother in focus node's parent. |
|shift | `focus node` | Moves focus node's parent position relative to its parent. |
| kill | `focus node` | Removes focus node window from tree. |
| mark | `view node` | Sets a bookmark on the current viewnode. |
| jump | `view node` | Restores a bookmark of viewnode set with `mark`. |

After building `foo-wm` I used it for about a year on and off. Being my first major serious C project; I learned to about `malloc`, `free`, and `memset` the hard way -- watching my windows crash and burn.

I've since stopped using `foo-wm` because I was too lazy to implement full [EWMH](http://standards.freedesktop.org/wm-spec/wm-spec-latest.html) window hinting, which substantially limited the number of applications I could use within `foo-wm` without annoyance.

When I go to build my next wm I'll likely scrap `foo-wm` and take a completely different approach1 to a meta-wm/DSL-based-wm altogether.

While Foo Wm may be dead, it spirit lives on in some form. The `0.1` release of [Bspwm](https://github.com/baskerville/bspwm), a popular tree-based wm within the Arch community, was influenced in part from the server/client architecture that I built into `foo-wm`2.


code:
---
archived: true
title: Keyboard Tiler
date: 2014-02-12
#description: Simple window tiling script for floating window managers.
description: Window Tiling Script
category: linux
status: 'production'
links:
    Source: https://github.com/mil/keyboard-tiler
    Python Port: https://github.com/ShadowKyogre/keyboard-tiler
sort_index: 101
---

[Keyboard Tiler](https://github.com/mil/keyboard-tiler) is a simple script which given 2 keys on your keyboard, maps a window to the screen using the keyboard grid as referent.

On a keyboard, the keys from **1** down to **Z** over to **/**, and up to **0** form a 4x10 grid. Given the 40 tiles on your keyboard, keyboard tiler maps the visual space of your screen to the grid of your keyboard.

 1	 2	 3	 4	 5	 6	 7	 8	 9	 0

 Q	 W	 E	 R	 T	 Y	 U	 I	 O	 P

 A	 S	 D	 F	 G	 H	 J	 K	 L	 ;

 Z	 X	 C	 V	 B	 N	 M	 <	 >	 /

So in the above example, hitting **1 and B** would place and resize a window to occupy the entire **left half your screen because that's the left half of your keyboard**. This works for any two keys.  My intention was to create an obvious and intuitive mental model for keyboard-driven window management.

I scripted around `xdotool` so I could focus on the logic of processing the two points and positioning the window on the screen rather than building out a full window manager. The [entire script](http://github.com/mil/keyboard-tiler/blob/master/keyboard-tiler.rb) works to calculate the variables used, ultimatly pushing to xdotool as:

```ruby
%x[xdotool getactivewindow windowmove --sync #{startX} #{decorationsHeight + startY}]
%x[xdotool getactivewindow windowsize --sync #{newWidth} #{newHeight - (decorationsHeight * 2)}]
```

Providing keybindings is another task separate from the logic of my key processing script. For the keybindings my first thought was to use [xchainkeys](http://code.google.com/p/xchainkeys). This way I could have emacs style key chording for pressing two keys in positioning.  To generate the keybindings for xchainkeys, I created the [logic to generate all permutations](https://github.com/mil/keyboard-tiler/blob/master/utils/generate-xchains.rb) for any two keys on the grid being pressed:

```ruby
$tiles = [
	[ '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' ],
	[ 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p' ],
	[ 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';' ],
	[ 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/' ]
]

def crawl(s)
	$tiles.each_with_index do |row, column|
		row.each_with_index do |cell, count|
			# This is 1 Permutation!
			# This will be hit 1560 times!
		end
	end
end

$tiles.each_with_index do |row, column|
	row.each_with_index do |cell, count|
		crawl(cell)
	end
end
```

You can also use this script with [Dmenu](http://tools.suckless.org/dmenu/) if your handy with pipes and the only disadvantage over xbindkeys is you have to hit enter after your two keys. An example of how to setup Dmenu can be found in the project's [README](http://github.com/mil/keyboard-tiler).


code:
---
archived: true
title: Markdown Tree
date: 2014-02-12
description: Markdown Site Generator
category: web
status: 'production'
sort_index: 2
links:
    'Source' : http://github.com/mil/markdown-tree
    'Example Site': http://markdown-tree.bladdo.net
    'My College Notes': http://notes.bladdo.net
footnotes:
    - Feel free to install a Procfile and deploy to Heroku or any Passanger-enabled web host.
    - 'Ask me to show you my failed start-up which I developed in college, evolving out of Markdown Tree, Mil Edit, and Mmvp.js.'
---

[Markdown Tree](http://github.com/mil/markdown-tree) is a small [Sinatra](http://www.sinatrarb.com)-based Ruby [script](https://github.com/mil/markdown-tree/blob/master/markdown-tree.rb) which serves a [folder of markdown files](https://github.com/mil/markdown-tree/tree/master/content) as a minimalistic [live styled site](http://markdown-tree.bladdo.net).  E.g. given the folder structure:

```
My Notes/
    Schedule.md
    History/
    Science/
        Some-Note.md
```

You'll get a dynamic site on `localhost:3000`1 with a navigation header for browsing looking like:

        My Notes

Schedule

History

Science

For a live example, visit [my notes from college](http://notes.bladdo.net) and compare against the [raw folder structure](http://github.com/mil/School) of my notes2.


code:
---
archived: true
title: Mil Edit
date: 2014-02-12
#description: Markdown list editor with a responsive feel.
description: Markdown List Editor
extended_description: Markdown List Editor
status: 'experimental'
category: web
sort_index: 0
links:
    'Source' :
        '1' : http://github.com/mil/mil-edit
        '2' : http://github.com/mil/mil-edit-2
    'Demo' :
        '1' : http://userbound.com/interfaces/Mil-Edit/1
        '2' : http://userbound.com/interfaces/Mil-Edit/2
footnotes:
    - 'In mil-edit, you edit one line at a time. The line your editing shows markdown attributes like, **star bolding** _underline italicizing_ and converts it styled after you navigate to a different line.'
    - 'My markdown library, dubbed mil-markdown, cut dependencies on 2 markdown libraries from the original mil-edit, by soley handling the import and exporting of a single markdown list.'
    - 'This a painful mistake in application development and one I had to learn the hard way.'
    - 'Ask me to show you a demo of my failed startup which wraps Mmvp.js and my Mil Edit 2 abondware!'
---
[Mil Edit](http://github.com/mil/mil-edit) is a small javascript markdown _list editor_ which supports basic text attributes (**bold**, _italic_), has undo/redo capabilities, and drag-and-drop.

The story goes that after realizing that most browser-based markdown editors are document editors, and not _list_ editors, I set out to make my own browser-based markdown _list_ editor!  I decided my editor would exclusively operate on a single markdown list and support text attributes in hackerish-style1.

Here's a demo of `mil-edit` embedded so you can get a feel for how it operates:

<link type="text/css" rel="stylesheet" property="stylesheet" href="/assets/prj_demo/mil_edit/css/style.css">

The [first version](https://github.com/mil/mil-edit) of `mil-edit` I implemented as a very simple 500-some-line [javascript singleton](https://github.com/mil/mil-edit/blob/master/js/mil_edit.js) which featured simple importing and exporting of a single markdown list using its `load` and `dump` functions.  However, I quickly became un-satisfied with the un-maintainable and monolithic architecture of `mil-edit`.

As such, I set out to make `mil-edit-2`. The driving force behind it's development was a cleaner internal architecture.  I separated out functionality into [small modules](https://github.com/mil/mil-edit-2/tree/master/js/app), wrote my own [markdown library](https://github.com/mil/mil-edit-2/blob/master/js/app/mil_markdown.js)2, and cleaned up the source a lot. Additionally, I added undo/redo and drag-and-drop capabilities to the feature set.

Mil Edit was featured in Linux Pro Magazine's May 2013 issue! That was several months after it's conception and release.

Unfortunately, regardless of the modularity I worked into `mil-edit-2`, my architecture was flawed. I wrote [tests](https://github.com/mil/mil-edit-2/tree/master/spec) _after_ the fact3 and eventually the startup which was the driving force behind the development of `mil-edit` failed4.

If I were to continue on `mil-edit` nowawadays, I would like to re-implement, start from scratch, writing [browserify](http://browserify.org/) modules and embrace TDD from day one. Oh yeah, I'd put focus on mobile-first.


code:
---
archived: true
title: Mmvp.js
date: 2014-02-12
description: 'µ-MVP Library'
status: 'production'
category: web
sort_index: 1
links:
    'Source' : http://github.com/mil/Mmvp.js
footnotes:
    - 'Binding as in specifying M->V callbacks; and not V->M callbacks. After all V->M logic is typically handled as consequent of user-interaction and thus should be specified in user-interface callbacks.'
    - 'Feel free to pop open web inspector and run on this page! Mmvp is in global scope.'
    - 'The demo code (74 SLOC) is only slighter heavier than the Mmvp library itself (45 SLOC)'
---

[Mmvp.js](http://github.com/mil/Mmvp.js) is a micro-MVP javascript library which allows you to bind presenter callbacks to be triggered in reaction to changes in a model.

Binding1 callbacks is as simple as passing a presenter callback function to `Mmvp`'s `set_action` function.  Once an action is bound, modifying the model dispatches the attached callback:

```js
var presenter = new Mmvp();
var model = {};

presenter.set_action({
    add: function(key, value) {
        console.log(
            "key '" + key + "' added with value '" + value + "'"
        );
    }
});

model.item_a = "hello";
presenter.sync(model);
// console.log: key 'item_a' added with value 'hello'</pre>
```

The above example2 shows binding of the `add` action callback. In addition to the `add` callback, the following callbacks may be bound to react to model changes:

|Action     |Arguments         | Dispatched when |
|:-------   |:-----------------|:-----------------------------------------|
|`add`      | `key`, `value`   | An item is added to the model               |
|`remove`   | `key`            | An item is removed from the model           |
|`update`   | `key`, `value`   | An item's value was udpated                 |
|`populate` | `key`, `value`   | The model goes from being empty to populated |
|`empty`    |                  | The model goes from being populated to empty  |

You can imagine how you might construct more complex views by putting to use DOM selectors and a templating engine in your action callbacks.

A MVP library explication wouldn't be complete without a TodoMVC demo. So, here's an archetypal TodoMVC demo coded up3 using `Mmvp`'s presenter callbacks. :

<link rel='stylesheet' href='/assets/prj_demo/mmvp.js/css/style.css' />

View

Model

        »
        <input type='text' placeholder='Item Name'>


code:
---
archived: true
title: Mustache.Io
date: 2014-02-12
description: Io Mustache Bindings
category: web
status: 'production'
sort_index: 3
links:
    'Source' : http://github.com/mil/mustache.io
    'Io Lang' : http://iolanguage.org/
    'All Bindings' : https://mustache.github.io/
---

[Mustache.Io](http://github.com/mil/mustache.io) is library for compiling [Mustache](https://mustache.github.io/) logicless templates in the [Io](http://iolanguage.org/) language.  It's usable in conjunction with _why's [yown](https://github.com/whymirror/yown) web-framework, or usable standalone.

Mustache.Io is designed to work both Io object and maps. It's simple api gives you two methods: `render` and `setDelimiters`:

|Method      |Arguments   |Description  |
|:------------|:------------|:-------------|
|`render` | `template`, `object`, `partials` | Renders the given template with the provided object and partials. |
|`setDelimiters` | `start_delimiter`, `end_delimiter` |  Sets the delimiters. |

For standard `Mustache` template rendering, a simple contrived example in `Io` might look like:

```io
doFile("lib/mustache.io")
obj := Object clone do(
  name := "Miles"
  age  := 20
)

template := "I am {{ name }}, age: {{ age }}"
Mustache render(template, obj)
```

yields:

```io
I am Miles, age: 20
```

That's pretty much all there is to it. Feel free to check out the [full documentation here](http://github.com/mil/mustache.io).


code:
---
archived: true
title: Cube
date: 2014-02-12
poster: /assets/prj_img/cube/poster.jpg
description: Portable speaker design
category: physical
links:
    'DXF Files': http://deskthority.net/wiki/Alps_SKCM_Cream_Damped
    'Openscad Files': http://deskthority.net/wiki/Alps_SKCM_Cream_Damped
sort_index: 1002
---

During the summer of my junior year at Virginia Tech, I stayed in Blacksburg and did the summer-session for Industrial Design minors. It was a relaxing, thoughtful, and productive summer. I learned to sketch perspective and developed an interest in [3d modeling](http://www.openscad.org/).

For our final project in the summer-session, I built a small portable speaker box dubbed the Cube.

I modeled The entire cube in `openscad` and combined the surfaces as dxf files `import`'ed into a modular master `openscad` composition.

Eventually when I was satisfied I cut the dxf layers out of masonite on our laser cutter. The final cube was assembled with wood glue.

![Initial perspective sketch lines](/assets/prj_img/cube/lines.jpg)

Initial perspective sketch lines.

![Persepective sketch](/assets/prj_img/cube/semi-finalized.jpg)

Apparently - I can sketch a box!

![Exploded view of Cube](/assets/prj_img/cube/exploded.png)

An exploded view of each of the panels of the Cube.

![Cube with iPod](/assets/prj_img/cube/1.jpg)

With an iPod


code:
---
archived: true
title: Piply
date: 2014-02-14
poster: /assets/prj_img/piply/cover.jpg
description: An audio player for the RPI
category: physical
links:
    'Source': http://github.com
    'Pi plate': http://github.com
sort_index: 1000
---
[Piply](https://github.com/mil/Piply) is a small `python2`, `alsa`, and `mplayer`-based audio playing interface for the Raspberry Pi and [pi plate](http://www.adafruit.com/product/1109) I created after failing to find a good mplayer wrapper interface to the pi plate.

Piply includes a small file-browsing interface, a playback screen in which you can speed up or slow down a song, and a configuration screen to adjust volume.

The user interface contains 3 screens:

- **Now**: shows the currently playing song
- **Lib**: a simple filebrowsing interface
- **Cfg**: volume control

![Animated Gif of Piply](/assets/prj_img/piply/piply.gif)

An animated GIF of the user interface of Piply.

I really love using the Raspberry Pi hooked up to my audio drive. Right now I run Arch Linux ARM on my Pi, and from boot all I have to do is `ssh` in and start `python2 Piply.py` to get it up and running (under a `dtach` session).

I plan to eventually write a small `systemd` script to automate the process all together to have an end-to-end audio player.

![Piply running on Rapsberry Pi](/assets/prj_img/piply/im.jpg)

My Raspberry Pi B+ running Piply.


code:
---
archived: true
title: Blank Thinkpad
date: 2014-05-12
poster: /assets/prj_img/blank_thinkpad/cover.jpg
description: Thinkpad lettering removal
category: physical
sort_index: 1003
---
One weekend while hacking away on my Thinkpad X40, I began to grow irritated by the lettering on my keys. I decided that I wanted blank keys on my Thinkpad ala a [Das keyboard](http://en.wikipedia.org/wiki/Das_Keyboard) or something of the sort. I search online briefly for places to buy blank keys, then decided to just start experimenting with blanking the keys myself.

The process I found to work to produce good consistent blank keys is made up of two steps. First, each key is removed from the keyboard and sanded with dull-grit sandpaper. Then a small amount of mineral oil is dabbed atop each key and spread to hide the sanding roughness.

![Sanding a key](/assets/prj_img/blank_thinkpad/sanding-key.jpg)
Keys are easier to sand in terms of traction when the sandpaper is place on the edge of a table.

![Dabbed mineral oil](/assets/prj_img/blank_thinkpad/dabbed-mineral-oil.jpg)
Only a small amount of mineral oil is needed for each key. I suggest [Wegman](http://www.wegmans.com/)'s brand for best results..

![Comparison of keys](/assets/prj_img/blank_thinkpad/two-keys.jpg)
A stock key on the left and a sanded and oiled key on the right.

When looking at the keyboard with fully blanked keys, you can tell it's non-stock, but it actually looks surprisingly professional. I found that it was best to smooth the hard edges on each key, making each key more smooth than concave. This plays better with the polishing as well.

![Finished keyboard](/assets/prj_img/blank_thinkpad/final-product.jpg)
The finished laptop keyboard in all its glory. I've moved the keycaps from my retired X40 to the X200s I currently use.


code:
---
title: Untitled Prints
date: 2014-05-12
description: One-off collection of OpenSCAD models
category: physical
sort_index: -1
archived: false
---

  Image
  Code

  Image
  Code

  Image
  Code

  Image
  Code

  Image
  Code


code:
---
title: Compiled Animations
date: 2018-10-13
description: One-off collection of SASS-based animations built with keyfram
category: physical
sort_index: -2
url: /assets/prj_compiled_animations/6
target: _blank
archived: false
---


code:
---
title: Keyfram
date: 2018-10-13
description: Modular Sass Animation Library
category: physical
sort_index: -3
url: http://keyfram.milesalan.com
target: _blank
archived: false
---


code:
---
title: Suckless Patches
date: 2020-02-08
description: Personal Library of Patches for Suckless Programs
category: none
sort_index: -5
archived: false
---
Here's my collection of custom patches for various [Suckless](http://suckless.org/philosophy/) software. If you find bugs with any of these patches please drop me a note. These patches are mostly hosted on the suckless site but also mirrored on my [repository here](http://github.com/mil/suckless-patches) as well for posterity.

**dmenu**

- [Dmenu highlight](https://tools.suckless.org/dmenu/patches/highlight): Show the individual characters of matched text in a different (highlighted) color.
- [Dmenu numbers](https://tools.suckless.org/dmenu/patches/numbers): Show the number of matched and total number of entries in the top right corner of dmenu.
- [Dmenu initialindex](https://github.com/mil/suckless-patches/blob/master/dmenu/dmenu-initialindex-4.9.diff): Start with an initial index based on command line flag.

**dwm**

- [Dwm bartabgroups](https://dwm.suckless.org/patches/bartabgroups): Show two seperate tab groups (based on nmaster) when tiling split at mfact location.
- [Dwm dragmfact](https://dwm.suckless.org/patches/dragmfact): Allows you to use the mouse to resize the mfact (split proportion) like in i3.
- [Dwm inplacerotate](https://dwm.suckless.org/patches/inplacerotate): Rotate all clients in the master or the stack.
- [Dwm transferall](https://dwm.suckless.org/patches/transfer/): Swap the master and the stack and adjust nmaster accordingly.
- [Dwm transfer](https://dwm.suckless.org/patches/transfer/): Transfer the currently focused client between the master & stack adjusting for nmaster accordingly.
- [Dwm multikey](https://dwm.suckless.org/patches/multikey/): Allows a single keybinding to run different actions based on number of presses.

**st**

- [St externalpipe-signal](https://st.suckless.org/patches/externalpipe-signal/): Allows utilizing an externalpipe command on the SIGUSR1 signal.
- [St invert](https://st.suckless.org/patches/invert/): Allows inverting the current colorscheme of st via keybinding.

**surf**

- [Surf externalpipe](https://surf.suckless.org/patches/externalpipe/): Pipe current webpage source through external command on keypress. See [blog post](https://milesalan.com/notes/surf-externalpipe-patch-browsing-via-dmenu/).
- [Surf externalpipe-signal](https://surf.suckless.org/patches/externalpipe-signal): Allows utilizing an externalpipe command on the SIGUSR1 signal.


code:
---
title: Perspective tool
date: 2020-02-10
description: Interactive vanishing point drawing tool. (2PP supported, 3PP & 1PP in progress)
category: none
sort_index: -6
url: http://perspectivetool.milesalan.com
target: _blank
archived: false
---


code:
---
title: Lisgd
date: 2020-04-18
description: Libinput synthetic gesture daemon (C)
category: none
sort_index: -7
url: https://git.sr.ht/~mil/lisgd
target: _blank
archived: false
---


code:
---
title: Archived
date: 2020-04-18
description: Old amateurish projects
category: none
sort_index: 888
url: /archived
target: _blank
archived: false
---


code:
---
title: Sxmo
date: 2021-05-03
description: Pinephone & mobile Linux UI based on simple & minimalistic software
category: none
sort_index: -10
url: http://sr.ht/~mil/Sxmo
target: _blank
archived: false
---


code:
---
title: Ppbbq10
date: 2021-05-20
description: Linux kernel module for arturo182's BB I2C keyboard compatible with the Pinephone
category: none
sort_index: 10
url: https://git.sr.ht/~mil/ppbbq10
target: _blank
archived: false
---


code:
---
title: Mepo
date: 2021-06-03
description: Fast, simple, hackable OSM map viewer for desktop & mobile Linux written in Zig
category: none
sort_index: -12
url: https://sr.ht/~mil/mepo
target: _blank
archived: false
---


code:
---
title: Framebufferphone
date: 2022-04-29
description: Experimental framebuffer based Linux phone UI; based on Zig & Oil
category: none
sort_index: -11
url: https://sr.ht/~mil/framebufferphone
target: _blank
archived: false
---


code:
<%= H.partial(:header, { :title => title, :section => section })  %>

<div class='
  post_content
  <% if(section == 'projects') %>
    project_<%= title.downcase.gsub(" ", "_") %>
  <% end %>
'>

<%= title %>

      <% if(defined?(description)) %><%= description %><% end %>

  <%= content %>

<%= H.partial(:footer) %>


code:
<%= H.partial(:header, { :title => title, :section => section })  %>

<%= H.partial(:listing, {
  :listing_items => listing_items,
  :name => title.capitalize,
  :home => false
}) %>

<%= H.partial(:footer) %>


code:
<%= H.partial(:header, { :title => title, :section => section })  %>

<div class='
  post_content
  <% if(section == 'projects') %>
    project_<%= title.downcase.gsub(" ", "_") %>
  <% end %>
'>

    <%# Date comes before for note posts %>
    <% if(section == 'notes') %>
    <time><%= H.datefmt(date) %></time>
    <% end  %>

<%= title %>

      <% if(defined?(description)) %><%= description %><% end %>

  <% if(defined?(poster)) %>
    <img class='poster-img' src="<%= poster %>" />
  <% end  %>

  <%= content %>

  <% if(defined?(footnotes)) %>

        <% footnotes.each do |f| %>

<%= f %>

        <% end %>

  <% end %>

<%= H.partial(:footer) %>


code:
<!doctype html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/all.css">
    <link rel="icon" href="/assets/favicon.ico">
    <title>
      <% if(defined?(title)) %><%= title %><% end %> ← Miles Alan
    </title>

      <% 10.times do |f| %>

<% end %>

      <a class='<%= H.nav_active("about", section) %>' href="/">About
      <a class='<%= H.nav_active("projects", section) %>' href="/projects">Projects
      <a class='<%= H.nav_active("notes", section) %>' href="/notes">Notes

    <content>


code:
    <% if(defined?(home) && home) %>All<% end %>

<%= name %>

  <div class='list list_type_<%= name.downcase %>'>
    <% listing_items.each do |item| %>
      <a
        href="<%= item[:url] %>"
        <% if(defined?(item[:yaml]['target'])) %>
          target="<%= item[:yaml]['target'] %>"
        <% end %>
      >
        <% if(name != "Projects" && name != "Archived") %>
          <time>
<%= item[:yaml]["date"] %>
</time>
        <% end %>

<%= item[:yaml]["title"] %>
        <% if(defined?(item[:yaml]['description'])) %>
          <%= item[:yaml]['description'] %>
        <% end %>

    <% end %>


code:
.post_content .about-img {
  float: right;
  max-width: 25%;
  width: 100%;
  margin: 0;
  margin-left: 20px;
  padding: 0;
  border-radius: 3px;
  border: 1px solid #888888;
}


code:
@import './vendor/reset.scss';
@import './vendor/highlight.scss';

@import './fns_and_mixins.scss';
@import './base.scss';
@import './post_content.scss';
@import './listing_items.scss';
@import './nav.scss';
@import './projects.scss';
@import './headers.scss';
@import './git_content.scss';
@import './about.scss';

.important-footer-message {
  font-size: 1px;
}

.top-bleed {
  width: 100%;
  height: 5px;
  display: block;
  //background-image: linear-gradient(
  //  -39deg, #f87f73 0%,
  //  #f87f73 60%, #292423 60%,
  //  #292423 60%
  //);
  position: relative;
  width: 100%;

  @for $i from 1 through 10 {
    div:nth-child(#{$i}) {
      position: absolute;
      background: adjust-hue(red, $i * 40);
      width: 10%;
      height: 5px;
      display: block;
      left: ($i - 1) * 10%;
    }
  }
}

content {
  display: block;
  max-width: 640px;
  padding: 0 20px;
  width: 100%;
  margin: 0 auto;
  box-sizing: border-box;
}


code:
html, body { margin: 0; }
html { font-size: 16px; }
h1, h2, h3, h4, h5 { font-weight: normal; }

body {
  background: white;
  margin-bottom: sp(4);
  font-weight: normal;
  font-family: LibreBaskerville;
  font-weight: normal;
  overflow-y: scroll;
  @include fz(md);
}


code:
$size_map: (xl: 1.602, lg: 1.266, md: 1, sm: 0.889, xs: 0.79);

@mixin fz($size, $multiplier: 1) {
  font-size: #{map-get($size_map, $size) * $multiplier}rem;
  line-height: 1.5;
}

@function sp($size, $multiplier: 1) {
  @return $size * 10px;
}

@function cl($c) {
  $colors: (
    dark: #888888,
    gray: #e8e8e8,
    blue: #002e7d
  );
  @return map-get($colors, $c);
}

@each $f in (Inconsolata, LibreBaskerville) {
  @each $w in (normal, bold) {
    @font-face {
      font-family: $f;
      src: url(assets/fnt/#{$f}-#{$w}.ttf);
      font-weight: $w;
    }
  }
}


code:
content.git-content {
  max-width: 960px;
  margin-top: 40px;
  @include fz(sm);

  font-family: monospace !important;

  h1, h2, h3, h4, h5, h6 {
    //font-size: 1em;
    margin: 0;
  }

  img, h1, h2 {
    vertical-align: top;
  }

  img {
    //margin-top: 5px;
    border: 0;
  }
  a {
    color: #002e7d;
    text-decoration: none;
  }

  table:first-of-type {
    tr:first-child {
      td:first-child {
        padding-left: 0;
      }
    }
  }

  tr.url td:last-child {
    //background: #f9f9f9;
    //display: inline;
  }

  a.d, a.h, a.i, a.line {
    text-decoration: none;
  }

  #blob a {
    color: #777;
  }

  #blob a:hover {
    color: blue;
    text-decoration: none;
  }

  table thead td {
    font-weight: bold;
  }

  #content {
    table td {
      padding: 2px 15px;
      //border-bottom: 1px solid #e8e8e8;
    }

    table tr td:first-child {
      padding-left: 0;
    }
    table tr td:last-child {
      padding-right: 0;
    }
  }

  table td {
    padding: 0 0.4em;
  }

  #content table td {
    vertical-align: top;
    white-space: nowrap;
  }

  #branches tr:hover td,
  #tags tr:hover td,
  #index tr:hover td,
  #log tr:hover td,
  #files tr:hover td {
    background-color: #eee;
  }

  #index tr td:nth-child(2),
  #tags tr td:nth-child(3),
  #branches tr td:nth-child(3),
  #log tr td:nth-child(2) {
    white-space: normal;
  }

  td.num {
    text-align: right;
  }

  .desc {
    color: #777;
  }

  hr {
    border: 0;
    border-top: 1px dotted #e8e8e8;
    height: 1px;
  }

  pre {
    font-family: monospace;
    overflow-x: auto;
  }

  pre a.h {
    color: #00a;
  }

  .A, span.i, pre a.i {
    color: #070;
  }

  .D, span.d, pre a.d {
    color: #e00;
  }

  pre a.h:hover, pre a.i:hover, pre a.d:hover {
    text-decoration: none;
  }
}


code:
h1 {
  span {
    display: inline-block;
    padding: sp(1) / 2;
    @include fz(xs);
    color: black;
    margin-top: sp(2);
    float: right;
  }
}

h2 {
  @include fz(md);
}

h4 {
  @include fz(sm);
  font-weight: bold;
}

.post_content {

h4 + p {
  margin-top: 0;
}
}

ul li {
    list-style: disc;
}

header {
  margin-top: sp(4);
  border-bottom: 1px dotted cl(gray);
  margin-bottom: sp(2);
  padding-bottom: sp(1);

  h2 {
    margin-top: 0;
    display: inline-block;
  }
  time {
    color: cl(dark);
    @include fz(xs);
  }
  .description {
    $s: 10px;
    color: cl(dark);
    @include fz(xs);
    display: block;
  }
}


code:
.listing_items {

  .list {
    $spc: 15px;
    display: table;
    clear: both;
    //border-bottom: 1px dotted cl(gray);
    @include fz(xs);
    padding-bottom: sp(1);

    a {
      display: table-row;
      text-decoration: none;
    }

    * + * { margin-top: sp(1) / 2; }

    h3, time {
      display: table-cell;
      vertical-align: middle;
      box-sizing: border-box;
      vertical-align: middle;
      min-height: sp(5);
    }

    h3, time > div { $s: 0.5; margin-bottom: sp($s); margin-top: sp($s); }
    &_type_projects h3 div, &_type_archived h3 div{
      $s: 0.75; margin-bottom: sp($s); margin-top: sp($s);
      &::after {
        display: block;
        width: 40px;
        height: 0px;
        border-top: 1px dotted cl(gray);
        margin-top: sp($s * 2);
        content: " ";
      }
    }
    &_type_projects a:last-child h3 div::after, &_type_archived a:last-child h3 div::after {
      display: none;
    }

    h3   {
      @include fz(sm);
      color: cl(blue);
      margin: 0;
      padding: 0;
      width: 100%;
    }
    time {
      @include fz(xs);
      color: cl(dark);
      div {

        border-right: 1px dotted cl(gray);
        margin-right: $spc;
        white-space: nowrap;
        $s: 15px;
      }
    }
    .description { display: block; color: cl(dark); @include fz(xs); }

    h3 {  }
    time div { padding-right: $spc; }

  }

  a.all {
    float: right;
    @include fz(xs);
    color: cl(blue);
    border-bottom: none;
    text-decoration: none;
  }
}


code:
nav {
  clear: both;
  text-align: right;
  @include fz(xs);
  margin: 0 20px;

  a {
    display: inline-block;
    border-left: 1px dotted cl(blue);
    text-decoration: none;
    padding: #{sp(1) / 3} sp(1);
    margin-top: sp(1);
    color: cl(dark);
    border-bottom: 1px dotted #e8e8e8;

    &:first-child {
      border-left: none;
    }

    &.active {
      color: cl(blue);
    }
  }
}


code:
.post_content {

  p { display: block; }
  strong, em { display: inline-block; }
  strong { font-weight: bold; }
  em { font-style: italic; }

  a {
    color: cl(blue);
    border-bottom: 1px solid #e8e8e8;
    text-decoration: none;
    &:hover { background: blue; color: white; }
  }

  .highlight pre {
    display: block;
    font-family: Inconsolata;
    border: 1px solid #e8e8e8;
    padding: sp(1);
    overflow-x: auto;
    @include fz(sm);

    .code pre {
      padding-left: 10px;
    }
  }

  p > code {
    border: 1px solid #e8e8e8;
    padding-left: 2px;
    padding-right: 2px;
    font-family: Inconsolata;
    border-radius: 3px;
  }

  > code {
    border: 1px solid #e8e8e8;
    padding: 2px;
    margin: 0 2px;
    border-radius: 2px;
  }

  ul {
    list-style-type: none;
    list-style-position: inside;
    margin-left: 20px;
    li {  }
  }

  p + ul {
    margin-top: sp(-1);
  }

  .footnotes {
    display: block;

    &::before {
      margin-top: sp(2);
      margin-bottom: sp(1);
      content: " ";
      display: block;
      height: 0px;
      width: 30%;

      border-top: 1px dotted #cfcfcf
    }

  }
  ol.footnotes-list {
    @include fz(sm);

    list-style: decimal;
    list-style-position: outside;
    padding-left: 15px;
    li {
      padding-top: sp(1);
    }
  }

  hr {
    border: none;
    border-top: 1px dashed #cfcfcf;
    height: 0;
    overflow: hidden;
    width: 100%;
    display: block;
  }

  p, .highlight, .caption, .block, img, hr, ul {
    display: block;
    margin-top: sp(2);
    margin-bottom: sp(2);
  }

  .caption {
    margin-top: sp(-1);
    margin-bottom: sp(2);
  }

  .caption {
    text-align: center;
    @include fz(xs);
  }

  sup {
    vertical-align: top;
    @include fz(xs);
  }

  img {
    width: 100%;
  }

  table {
    border: 1px solid #e8e8e8;
    td, th {
      border-top: 1px solid #e8e8e8;
      border-right: 1px solid #e8e8e8;
      padding: 5px;
    }
    @include fz(sm);
  }
}


code:
//
//
// > Interfaces/Mmvp.js
body section#demo {
  border: 1px dotted #cfcfcf;
  border-radius: 3px;
  nav {
    padding: 14px 0;
    h3 {
      padding: 0 10px;
      &.active {
        &::before {
          top: 26px;
        }
        &::after {
          bottom: -14px;
        }
      }
    }
  }
  pre {
    border: none;
  }
}

// < Interfaces/Mmvp.js

//
//
// > Interfaces/Asciiw
body section#demo {}

.ascii-frames {
  text-align: center;
}

// < Interfaces/Asciiw

//
//
// > Interfaces/Markdown Tree
#markdown-tree {
  a {
    border-bottom: none;
    padding: 0px 5px 0px 20px;
    background: url("/assets/prj_img/markdown_tree/folder.png") no-repeat left center;
    &:last-child {
      color: black;
      font-size: 12px;
      line-height: 22px;
      height: 22px;
      display: inline-block;
    }
  }
  #path {
    border: 1px solid #dfdfdf;
    padding: 5px 10px;
    background: #eaeaea;
  }
  #menu {
    border: 1px solid #dfdfdf;
    border-top: none;
    padding: 5px 10px;
    margin-bottom: 5px;
    background: #f4f4f4;
  }
  #menu ul {
    margin: 0;
    border: none;
    li {
      background: url("/images/page.png") no-repeat left center;
      display: inline-block;
      list-style: none;
      font-size: 10px;
      padding-left: 20px;
      padding-right: 10px;
      line-height: 10px;
      border-right: 1px dotted #d4d4d4;
      &.page a {
        background-image: url("/assets/prj_img/markdown_tree/page.png");
      }
      &.folder a {
        background-image: url("/assets/prj_img/markdown_tree/folder.png");
      }
      &.current a {
        background-image: url("/assets/prj_img/markdown_tree/page_edit.png");
        a {
          color: black;
        }
      }
    }
  }
}

// < Interfaces/Markdown Tree

//
//
// > Interfaces/Mil Edit
#editor {
  box-sizing: border-box;
  ul li {
    list-style-type: none;
  }
}

// < Interfaces/Mil Edit

//
//
// > Interfaces/Keyboard Tiler
#keyboard {
  table-layout: fixed;
  border: 1px dotted #e8e8e8;
  width: 100%;
  box-sizing: border-box;
  tr {
    margin: 10px;
    background: #e8e8e8;
    td {
      overflow: hidden;
      background: #fff;
      padding: 8px 0;
      text-align: center;
      border: 1px dashed #ccc;
      margin: 5px;
      width: 100%;
      &.act {
        font-weight: bold;
      }
      &.hit {
        background: #eee;
      }
      //&:last-child

    }
  }
}

// < Interfaces/Keyboard Tiler

// > Untitled prints
.project_untitled_prints {
  .item {
    display: block;
    overflow: hidden;
    padding-bottom: sp(4);
    display: block;
    clear: both;
  }
  .img {
    clear: both;
    display: block;
    padding: 0;
    max-height: 150px;
    width: 100%;
    img {
      margin-top: -30px;
    }
    overflow: hidden;
    &:hover {
      background-color: white;
      background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABZJREFUeNpi2r9//38gYGAEESAAEGAAasgJOgzOKCoAAAAASUVORK5CYII=);
    }
  }
  a:not(.item) {
    @include fz(sm);
    float: right;
    margin-left: 10px;
    margin-top: 5px;

  }

}
// < Untitled prints


code:
.highlight table td { padding: 5px; }
.highlight table pre { margin: 0; }
.highlight .cm {
  color: #999988;
  font-style: italic;
}
.highlight .cp {
  color: #999999;
  font-weight: bold;
}
.highlight .c1 {
  color: #999988;
  font-style: italic;
}
.highlight .cs {
  color: #999999;
  font-weight: bold;
  font-style: italic;
}
.highlight .c, .highlight .cd {
  color: #999988;
  font-style: italic;
}
.highlight .err {
  color: #a61717;
  background-color: #e3d2d2;
}
.highlight .gd {
  color: #000000;
  background-color: #ffdddd;
}
.highlight .ge {
  color: #000000;
  font-style: italic;
}
.highlight .gr {
  color: #aa0000;
}
.highlight .gh {
  color: #999999;
}
.highlight .gi {
  color: #000000;
  background-color: #ddffdd;
}
.highlight .go {
  color: #888888;
}
.highlight .gp {
  color: #555555;
}
.highlight .gs {
  font-weight: bold;
}
.highlight .gu {
  color: #aaaaaa;
}
.highlight .gt {
  color: #aa0000;
}
.highlight .kc {
  color: #000000;
  font-weight: bold;
}
.highlight .kd {
  color: #000000;
  font-weight: bold;
}
.highlight .kn {
  color: #000000;
  font-weight: bold;
}
.highlight .kp {
  color: #000000;
  font-weight: bold;
}
.highlight .kr {
  color: #000000;
  font-weight: bold;
}
.highlight .kt {
  color: #445588;
  font-weight: bold;
}
.highlight .k, .highlight .kv {
  color: #000000;
  font-weight: bold;
}
.highlight .mf {
  color: #009999;
}
.highlight .mh {
  color: #009999;
}
.highlight .il {
  color: #009999;
}
.highlight .mi {
  color: #009999;
}
.highlight .mo {
  color: #009999;
}
.highlight .m, .highlight .mb, .highlight .mx {
  color: #009999;
}
.highlight .sb {
  color: #d14;
}
.highlight .sc {
  color: #d14;
}
.highlight .sd {
  color: #d14;
}
.highlight .s2 {
  color: #d14;
}
.highlight .se {
  color: #d14;
}
.highlight .sh {
  color: #d14;
}
.highlight .si {
  color: #d14;
}
.highlight .sx {
  color: #d14;
}
.highlight .sr {
  color: #009926;
}
.highlight .s1 {
  color: #d14;
}
.highlight .ss {
  color: #990073;
}
.highlight .s {
  color: #d14;
}
.highlight .na {
  color: #008080;
}
.highlight .bp {
  color: #999999;
}
.highlight .nb {
  color: #0086B3;
}
.highlight .nc {
  color: #445588;
  font-weight: bold;
}
.highlight .no {
  color: #008080;
}
.highlight .nd {
  color: #3c5d5d;
  font-weight: bold;
}
.highlight .ni {
  color: #800080;
}
.highlight .ne {
  color: #990000;
  font-weight: bold;
}
.highlight .nf {
  color: #990000;
  font-weight: bold;
}
.highlight .nl {
  color: #990000;
  font-weight: bold;
}
.highlight .nn {
  color: #555555;
}
.highlight .nt {
  color: #000080;
}
.highlight .vc {
  color: #008080;
}
.highlight .vg {
  color: #008080;
}
.highlight .vi {
  color: #008080;
}
.highlight .nv {
  color: #008080;
}
.highlight .ow {
  color: #000000;
  font-weight: bold;
}
.highlight .o {
  color: #000000;
  font-weight: bold;
}
.highlight .w {
  color: #bbbbbb;
}
.highlight {
  background-color: #f8f8f8;
}


code:
/* http://meyerweb.com/eric/tools/css/reset/ */
/* v1.0 | 20080212 */

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, font, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td {
	margin: 0;
	padding: 0;
	border: 0;
	outline: 0;
	font-size: 100%;
	vertical-align: baseline;
	background: transparent;
}
body {
	line-height: 1;
}
ol, ul {
	list-style: none;
}
blockquote, q {
	quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
	content: '';
	content: none;
}

/* remember to define focus styles! */
:focus {
	outline: 0;
}

/* remember to highlight inserts somehow! */
ins {
	text-decoration: none;
}
del {
	text-decoration: line-through;
}

/* tables still need 'cellspacing="0"' in the markup */
table {
	border-collapse: collapse;
	border-spacing: 0;
}


code:
# coding=utf-8
# !/usr/bin/python
import sys
sys.path.append('..')
from base.spider import Spider
import json
import re
import urllib

class Spider(Spider):  # 元类 默认的元类 type
    def getName(self):
        return "Alist"

    def init(self, extend=""):
        print("============{0}============".format(extend))
        pass

    def isVideoFormat(self, url):
        pass

    def manualVideoCheck(self):
        pass

    def homeContent(self, filter):
        result = {}
        cateManual = {
            "小雅": "http://43.139.29.179:5244",
            "七米蓝": "https://al.chirmyram.com",
            "梅花盘": "https://pan.142856.xyz/OneDrive",
            "触光云盘": "https://pan.ichuguang.com",
            "小孟资源": "https://8023.haohanba.cn/小孟丨资源大合集/无损音乐",
            "资源小站": "https://960303.xyz/ali",
            "轻弹浅唱": "https://g.xiang.lol",
            "小兵组网盘视频": "https://6vv.app"
        }
        classes = []
        for k in cateManual:
            classes.append({
                'type_name': k,
				"type_flag": "1",
                'type_id': cateManual[k]
            })
        result['class'] = classes
        if (filter):
            result['filters'] = self.config['filter']
        return result

    def homeVideoContent(self):
        result = {
            'list': []
        }
        return result

    ver = ''
    baseurl = ''
    def getVersion(self, gtid):
        param = {
            "path": '/'
        }
        if gtid.count('/') == 2:
            gtid = gtid + '/'
        baseurl = re.findall(r"http.*://.*?/", gtid)[0]
        ver = self.fetch(baseurl + 'api/public/settings', param)
        vjo = json.loads(ver.text)['data']
        if type(vjo) is dict:
            ver = 3
        else:
            ver = 2
        self.ver = ver
        self.baseurl = baseurl

    def categoryContent(self, tid, pg, filter, extend):
        result = {}
        if tid.count('/') == 2:
            tid = tid + '/'
        nurl = re.findall(r"http.*://.*?/", tid)[0]
        if self.ver == '' or self.baseurl != nurl:
            self.getVersion(tid)
        ver = self.ver
        baseurl = self.baseurl
        if tid.count('/') == 2:
            tid = tid + '/'
        pat = tid.replace(baseurl,"")
        param = {
            "path": '/' + pat
        }
        if ver == 2:
            rsp = self.postJson(baseurl + 'api/public/path', param)
            jo = json.loads(rsp.text)
            vodList = jo['data']['files']
        elif ver == 3:
            rsp = self.postJson(baseurl + 'api/fs/list', param)
            jo = json.loads(rsp.text)
            vodList = jo['data']['content']

        videos = []
        cid = ''
        purl = ''
        svodList = str(vodList)
        lenvodList = len(vodList)
        substr = str(re.findall(r"\'name\': \'(.*?)\'", svodList))
        foldernum = svodList.count('\'type\': 1')
        filenum = lenvodList - foldernum
        for vod in vodList:
            if ver == 2:
                img = vod['thumbnail']
            elif ver == 3:
                img = vod['thumb']
            if len(img) == 0:
                if vod['type'] == 1:
                    img = "http://img1.3png.com/281e284a670865a71d91515866552b5f172b.png"
            if pat != '':
                aid = pat + '/'
            else:
                aid = pat
            if vod['type'] == 1:
                tag = "folder"
                remark = "文件夹"
                cid = baseurl + aid + vod['name']
            #计算文件大小
            else:
                size = vod['size']
                if size > 1024 * 1024 * 1024 * 1024.0:
                    fs = "TB"
                    sz = round(size / (1024 * 1024 * 1024 * 1024.0), 2)
                elif size > 1024 * 1024 * 1024.0:
                    fs = "GB"
                    sz = round(size / (1024 * 1024 * 1024.0), 2)
                elif size > 1024 * 1024.0:
                    fs = "MB"
                    sz = round(size / (1024 * 1024.0), 2)
                elif size > 1024.0:
                    fs = "KB"
                    sz = round(size / (1024.0), 2)
                else:
                    fs = "KB"
                    sz = round(size / (1024.0), 2)
                tag = "file"
                remark = str(sz) + fs
                cid = baseurl + aid + vod['name']
                # 开始爬视频与字幕
                if filenum < 150:
                    if 'mp4' in vod['name'] or 'mkv' in vod['name'] or 'TS' in vod['name'] or 'flv' in vod['name'] or 'rmvb' in vod['name'] or 'mp3' in vod['name'] or 'flac' in vod['name'] or 'wav' in vod['name'] or 'wma' in vod['name'] or 'dff' in vod['name']:
                        vodurl = vod['name']
                        # 开始爬字幕
                        cid = '###'
                        subname = re.findall(r"(.*)\.", vod['name'])[0]
                        if filenum == 2:
                            if '.ass' in substr:
                                sub = re.findall(r"'(.*).ass", substr)[0]
                                subt = '@@@' + sub + '.ass'
                                if ',' in sub:
                                    sub = re.findall(r"', '(.*).ass", substr)[0]
                                    subt = '@@@' + sub + '.ass'
                            if '.srt' in substr:
                                sub = re.findall(r"'(.*).srt", substr)[0]
                                subt = '@@@' + sub + '.srt'
                                if ',' in sub:
                                    sub = re.findall(r"', '(.*).srt", substr)[0]
                                    subt = '@@@' + sub + '.srt'
                        else:
                            if subname + '.ass' in substr:
                                subt = '@@@' + subname + '.ass'
                            elif subname + '.srt' in substr:
                                subt = '@@@' + subname + '.srt'
                        # 合并链接
                        if 'subt' in locals().keys():
                            purl = purl + '{0}{1}#'.format(vodurl, subt)
                        else:
                            purl = purl + '{0}#'.format(vodurl)
                else:
                    subname = re.findall(r"(.*)\.", vod['name'])[0]
                    if subname + '.ass' in substr:
                        subt = '@@@' + subname + '.ass'
                        cid = cid + subt
                    elif subname + '.srt' in substr:
                        subt = '@@@' + subname + '.srt'
                        cid = cid + subt
            videos.append({
                "vod_id":  cid,
                "vod_name": vod['name'],
                "vod_pic": img,
                "vod_tag": tag,
                "vod_remarks": remark
            })
        if 'purl' in locals().keys():
            purl = baseurl + aid + '+++' + purl
            for i in range(foldernum, lenvodList):
                if videos[i]['vod_id'] == '###':
                    videos[i]['vod_id'] = purl
        result['list'] = videos
        result['page'] = 1
        result['pagecount'] = 1
        result['limit'] = 999
        result['total'] = 999999
        return result

    def detailContent(self, array):
        id = array[0]
        if '+++' in id:
            ids = id.split('+++')
            durl = ids[0]
            vsList = ids[1].strip('#').split('#')
            vsurl = ''
            for vs in vsList:
                if '@@@' in vs:
                    dvs = vs.split('@@@')
                    vname = dvs[0]
                    vurl = durl + dvs[0]
                    surl = durl + dvs[1]
                    vsurl = vsurl + '{0}${1}@@@{2}#'.format(vname, vurl, surl)
                else:
                    vurl = durl + vs
                    vsurl = vsurl + '{0}${1}#'.format(vs, vurl)
            url = vsurl
        else:
            durl = id
        if self.ver == '' or self.baseurl == '':
            self.getVersion(durl)
        baseurl = self.baseurl
        if '+++' in id:
            vid = durl.replace(baseurl, "").strip('/')
        else:
            vid = durl.replace(re.findall(r".*/", durl)[0], "")
            url = vid + '$' + id
        vod = {
            "vod_id": vid,
            "vod_name": vid,
            "vod_pic": '',
            "vod_tag": '',
            "vod_play_from": "播放",
            "vod_play_url": url
        }
        result = {
            'list': [
                vod
            ]
        }
        return result

    def searchContent(self, key, quick):
        result = {
            'list': []
        }
        return result

    def playerContent(self, flag, id, vipFlags):
        result = {}
        url = ''
        subturl = ''
        ifsub = '@@@' in id
        if ifsub is True:
            ids = id.split('@@@')
            if self.ver == '' or self.baseurl == '':
                self.getVersion(ids[1])
            ver = self.ver
            baseurl = self.baseurl
            fileName = ids[1].replace(baseurl, "")
            vfileName = ids[0].replace(baseurl, "")
            param = {
                "path": '/' + fileName,
                "password": "",
                "page_num": 1,
                "page_size": 100
            }
            vparam = {
                "path": '/' + vfileName,
                "password": "",
                "page_num": 1,
                "page_size": 100
            }
            if ver == 2:
                rsp = self.postJson(baseurl + 'api/public/path', param)
                jo = json.loads(rsp.text)
                vodList = jo['data']['files'][0]
                subturl = vodList['url']
                vrsp = self.postJson(baseurl + 'api/public/path', vparam)
                vjo = json.loads(vrsp.text)
                vList = vjo['data']['files'][0]
                url = vList['url']
            elif ver == 3:
                rsp = self.postJson(baseurl + 'api/fs/get', param)
                jo = json.loads(rsp.text)
                vodList = jo['data']
                subturl = vodList['raw_url']
                vrsp = self.postJson(baseurl + 'api/fs/get', vparam)
                vjo = json.loads(vrsp.text)
                vList = vjo['data']
                url = vList['raw_url']
            if subturl.startswith('http') is False:
                head = re.findall(r"h.*?:", baseurl)[0]
                subturl = head + subturl
            if url.startswith('http') is False:
                head = re.findall(r"h.*?:", baseurl)[0]
                url = head + url
            urlfileName = urllib.parse.quote(fileName)
            subturl = subturl.replace(fileName, urlfileName)
            urlvfileName = urllib.parse.quote(vfileName)
            url = url.replace(vfileName, urlvfileName)
            result['subt'] = subturl
        else:
            if self.ver == '' or self.baseurl == '':
                self.getVersion(id)
            ver = self.ver
            baseurl = self.baseurl
            vfileName = id.replace(baseurl, "")
            vparam = {
                "path": '/' + vfileName,
                "password": "",
                "page_num": 1,
                "page_size": 100
            }
            if ver == 2:
                vrsp = self.postJson(baseurl + 'api/public/path', vparam)
                vjo = json.loads(vrsp.text)
                vList = vjo['data']['files'][0]
                driver = vList['driver']
                url = vList['url']
            elif ver == 3:
                vrsp = self.postJson(baseurl + 'api/fs/get', vparam)
                vjo = json.loads(vrsp.text)
                vList = vjo['data']
                url = vList['raw_url']
                driver = vList['provider']
            if url.startswith('http') is False:
                head = re.findall(r"h.*?:", baseurl)[0]
                url = head + url
            urlvfileName = urllib.parse.quote(vfileName)
            url = url.replace(vfileName, urlvfileName)
            if driver == 'Baidu.Disk':
                result["header"] = {"User-Agent": "pan.baidu.com"}
        result["parse"] = 0
        result["playUrl"] = ''
        result["url"] = url

        return result

    config = {
        "player": {},
        "filter": {}
    }
    header = {}

    def localProxy(self, param):
        return [200, "video/MP2T", action, ""]

repo_name: jiory/TVbox

license: unknown


code:
# coding=utf-8
# !/usr/bin/python
import sys
sys.path.append('..')
from base.spider import Spider
import json
import re
import urllib

class Spider(Spider):  # 元类 默认的元类 type
    def getName(self):
        return "Alist"

    def init(self, extend=""):
        print("============{0}============".format(extend))
        pass

    def isVideoFormat(self, url):
        pass

    def manualVideoCheck(self):
        pass

    def homeContent(self, filter):
        result = {}
        cateManual = {
            "七米蓝": "https://al.chirmyram.com",
            "xiaoya": "http://43.139.29.179:5244",
            "梅花盘": "https://pan.142856.xyz/OneDrive",
            "触光云盘": "https://pan.ichuguang.com",
            "小孟资源": "https://8023.haohanba.cn/小孟丨资源大合集/无损音乐",
            "资源小站": "https://960303.xyz/ali",
            "轻弹浅唱": "https://g.xiang.lol",
            "小兵组网盘视频": "https://6vv.app"
        }
        classes = []
        for k in cateManual:
            classes.append({
                'type_name': k,
				"type_flag": "1",
                'type_id': cateManual[k]
            })
        result['class'] = classes
        if (filter):
            filters = {}
            for lk in cateManual:
                link = cateManual[lk]
                filters.update({
                    link: [{"key": "nm", "name": "名        称", "value": [{"n": "正序", "v": "False"},{"n": "反序", "v": "True"}]},{"key": "sz", "name": "大        小", "value": [{"n": "升序", "v": "False"},{"n": "降序", "v": "True"}]},{"key": "tp", "name": "类        型", "value": [{"n": "升序", "v": "False"},{"n": "降序", "v": "True"}]},{"key": "tm", "name": "修改时间", "value": [{"n": "升序", "v": "False"},{"n": "降序", "v": "True"}]}]
                })
            result['filters'] = filters
        return result

    def homeVideoContent(self):
        result = {
            'list': []
        }
        return result

    ver = ''
    baseurl = ''
    def getVersion(self, gtid):
        param = {
            "path": '/'
        }
        if gtid.count('/') == 2:
            gtid = gtid + '/'
        baseurl = re.findall(r"http.*://.*?/", gtid)[0]
        ver = self.fetch(baseurl + 'api/public/settings', param)
        vjo = json.loads(ver.text)['data']
        if type(vjo) is dict:
            ver = 3
        else:
            ver = 2
        self.ver = ver
        self.baseurl = baseurl

    def categoryContent(self, tid, pg, filter, extend):
        result = {}
        if tid.count('/') == 2:
            tid = tid + '/'
        nurl = re.findall(r"http.*://.*?/", tid)[0]
        if self.ver == '' or self.baseurl != nurl:
            self.getVersion(tid)
        ver = self.ver
        baseurl = self.baseurl
        if tid.count('/') == 2:
            tid = tid + '/'
        pat = tid.replace(baseurl,"")
        param = {
            "path": '/' + pat
        }
        if ver == 2:
            rsp = self.postJson(baseurl + 'api/public/path', param)
            jo = json.loads(rsp.text)
            vodList = jo['data']['files']
        elif ver == 3:
            rsp = self.postJson(baseurl + 'api/fs/list', param)
            jo = json.loads(rsp.text)
            vodList = jo['data']['content']
        ovodList = vodList
        if len(extend) != 0:
            if 'tp' in extend:
                fl = 'type'
                if extend['tp'] == "True":
                    key = True
                if extend['tp'] == "False":
                    key = False
                vodList.sort(key=lambda x: (x['{0}'.format(fl)]), reverse=key)
            elif 'sz' in extend:
                fl = 'size'
                if extend['sz'] == "True":
                    key = True
                if extend['sz'] == "False":
                    key = False
                vodList.sort(key=lambda x: (x['{0}'.format(fl)]), reverse=key)
            elif 'nm' in extend:
                fl = 'name'
                if extend['nm'] == "True":
                    key = True
                if extend['nm'] == "False":
                    key = False
                vodList.sort(key=lambda x: (x['{0}'.format(fl)]), reverse=key)
            elif 'tm' in extend:
                if ver == 2:
                    fl = 'updated_at'
                elif ver == 3:
                    fl = 'modified'
                if extend['tm'] == "True":
                    key = True
                if extend['tm'] == "False":
                    key = False
                vodList.sort(key=lambda x: (x['{0}'.format(fl)]), reverse=key)
            else:
                vodList = ovodList
        else:
            vodList = ovodList
        videos = []
        cid = ''
        purl = ''
        svodList = str(vodList)
        lenvodList = len(vodList)
        substr = str(re.findall(r"\'name\': \'(.*?)\'", svodList))
        foldernum = svodList.count('\'type\': 1')
        filenum = lenvodList - foldernum
        for vod in vodList:
            if ver == 2:
                img = vod['thumbnail']
            elif ver == 3:
                img = vod['thumb']
            if len(img) == 0:
                if vod['type'] == 1:
                    img = "http://img1.3png.com/281e284a670865a71d91515866552b5f172b.png"
            if pat != '':
                aid = pat + '/'
            else:
                aid = pat
            if vod['type'] == 1:
                tag = "folder"
                remark = "文件夹"
                cid = baseurl + aid + vod['name']
            #计算文件大小
            else:
                size = vod['size']
                if size > 1024 * 1024 * 1024 * 1024.0:
                    fs = "TB"
                    sz = round(size / (1024 * 1024 * 1024 * 1024.0), 2)
                elif size > 1024 * 1024 * 1024.0:
                    fs = "GB"
                    sz = round(size / (1024 * 1024 * 1024.0), 2)
                elif size > 1024 * 1024.0:
                    fs = "MB"
                    sz = round(size / (1024 * 1024.0), 2)
                elif size > 1024.0:
                    fs = "KB"
                    sz = round(size / (1024.0), 2)
                else:
                    fs = "KB"
                    sz = round(size / (1024.0), 2)
                tag = "file"
                remark = str(sz) + fs
                cid = baseurl + aid + vod['name']
                # 开始爬视频与字幕
                if filenum < 150:
                    if 'mp4' in vod['name'] or 'mkv' in vod['name'] or 'TS' in vod['name'] or 'flv' in vod['name'] or 'rmvb' in vod['name'] or 'mp3' in vod['name'] or 'flac' in vod['name'] or 'wav' in vod['name'] or 'wma' in vod['name'] or 'dff' in vod['name']:
                        vodurl = vod['name']
                        # 开始爬字幕
                        cid = '###'
                        subname = re.findall(r"(.*)\.", vod['name'])[0]
                        if filenum == 2:
                            if '.ass' in substr:
                                sub = re.findall(r"'(.*).ass", substr)[0]
                                subt = '@@@' + sub + '.ass'
                                if ',' in sub:
                                    sub = re.findall(r"', '(.*).ass", substr)[0]
                                    subt = '@@@' + sub + '.ass'
                            if '.srt' in substr:
                                sub = re.findall(r"'(.*).srt", substr)[0]
                                subt = '@@@' + sub + '.srt'
                                if ',' in sub:
                                    sub = re.findall(r"', '(.*).srt", substr)[0]
                                    subt = '@@@' + sub + '.srt'
                        else:
                            if '.ass' in substr:
                                if '(' in subname or ')' in subname:
                                    subname = subname.replace("(","\(").replace(")","\)")
                                sub = re.findall(r"{0}.*?.ass".format(subname), substr)[0]
                                if ',' in sub:
                                    lsub = sub.replace('\'',"").split(', ')
                                    sub = lsub[len(lsub)-1]
                                if '\(' in subname or '\)' in subname:
                                    subname = subname.replace("\(", "(").replace("\)", ")")
                                if subname in sub:
                                    subt = '@@@' + sub
                            elif '.srt' in substr:
                                if '(' in subname or ')' in subname:
                                    subname = subname.replace("(","\(").replace(")","\)")
                                sub = re.findall(r'{0}.*?.srt'.format(subname), substr)[0]
                                if ',' in sub:
                                    lsub = sub.replace('\'',"").split(', ')
                                    sub = lsub[len(lsub)-1]
                                if '\(' in subname or '\)' in subname:
                                    subname = subname.replace("\(", "(").replace("\)", ")")
                                if subname in sub:
                                    subt = '@@@' + sub
                        # 合并链接
                        if 'subt' in locals().keys():
                            purl = purl + '{0}{1}#'.format(vodurl, subt)
                        else:
                            purl = purl + '{0}#'.format(vodurl)
                else:
                    subname = re.findall(r"(.*)\.", vod['name'])[0]
                    if '.ass' in substr:
                        if '(' in subname or ')' in subname:
                            subname = subname.replace("(","\(").replace(")","\)")
                        sub = re.findall(r"{0}.*?.ass".format(subname), substr)[0]
                        if ',' in sub:
                            lsub = sub.replace('\'', "").split(', ')
                            sub = lsub[len(lsub) - 1]
                        if '\(' in subname or '\)' in subname:
                            subname = subname.replace("\(", "(").replace("\)", ")")
                        if subname in sub:
                            subt = '@@@' + sub
                        cid = cid + subt
                    elif '.srt' in substr:
                        if '(' in subname or ')' in subname:
                            subname = subname.replace("(","\(").replace(")","\)")
                        sub = re.findall(r"{0}.*?.srt".format(subname), substr)[0]
                        if ',' in sub:
                            lsub = sub.replace('\'', "").split(', ')
                            sub = lsub[len(lsub) - 1]
                        if '\(' in subname or '\)' in subname:
                            subname = subname.replace("\(", "(").replace("\)", ")")
                        if subname in sub:
                            subt = '@@@' + sub
            videos.append({
                "vod_id":  cid,
                "vod_name": vod['name'],
                "vod_pic": img,
                "vod_tag": tag,
                "vod_remarks": remark
            })
        if 'purl' in locals().keys():
            purl = baseurl + aid + '+++' + purl
            for i in range(foldernum, lenvodList):
                if videos[i]['vod_id'] == '###':
                    videos[i]['vod_id'] = purl
        result['list'] = videos
        result['page'] = 1
        result['pagecount'] = 1
        result['limit'] = 999
        result['total'] = 999999
        return result

    def detailContent(self, array):
        id = array[0]
        if '+++' in id:
            ids = id.split('+++')
            durl = ids[0]
            vsList = ids[1].strip('#').split('#')
            vsurl = ''
            for vs in vsList:
                if '@@@' in vs:
                    dvs = vs.split('@@@')
                    vname = dvs[0]
                    vurl = durl + dvs[0]
                    surl = durl + dvs[1]
                    vsurl = vsurl + '{0}${1}@@@{2}#'.format(vname, vurl, surl)
                else:
                    vurl = durl + vs
                    vsurl = vsurl + '{0}${1}#'.format(vs, vurl)
            url = vsurl
        else:
            durl = id
        if self.ver == '' or self.baseurl == '':
            self.getVersion(durl)
        baseurl = self.baseurl
        if '+++' in id:
            vid = durl.replace(baseurl, "").strip('/')
        else:
            vid = durl.replace(re.findall(r".*/", durl)[0], "")
            url = vid + '$' + id
        vod = {
            "vod_id": vid,
            "vod_name": vid,
            "vod_pic": '',
            "vod_tag": '',
            "vod_play_from": "播放",
            "vod_play_url": url
        }
        result = {
            'list': [
                vod
            ]
        }
        return result

    def searchContent(self, key, quick):
        result = {
            'list': []
        }
        return result

    def playerContent(self, flag, id, vipFlags):
        result = {}
        url = ''
        subturl = ''
        ifsub = '@@@' in id
        if ifsub is True:
            ids = id.split('@@@')
            if self.ver == '' or self.baseurl == '':
                self.getVersion(ids[1])
            ver = self.ver
            baseurl = self.baseurl
            fileName = ids[1].replace(baseurl, "")
            vfileName = ids[0].replace(baseurl, "")
            param = {
                "path": '/' + fileName,
                "password": "",
                "page_num": 1,
                "page_size": 100
            }
            vparam = {
                "path": '/' + vfileName,
                "password": "",
                "page_num": 1,
                "page_size": 100
            }
            if ver == 2:
                rsp = self.postJson(baseurl + 'api/public/path', param)
                jo = json.loads(rsp.text)
                vodList = jo['data']['files'][0]
                subturl = vodList['url']
                vrsp = self.postJson(baseurl + 'api/public/path', vparam)
                vjo = json.loads(vrsp.text)
                vList = vjo['data']['files'][0]
                url = vList['url']
            elif ver == 3:
                rsp = self.postJson(baseurl + 'api/fs/get', param)
                jo = json.loads(rsp.text)
                vodList = jo['data']
                subturl = vodList['raw_url']
                vrsp = self.postJson(baseurl + 'api/fs/get', vparam)
                vjo = json.loads(vrsp.text)
                vList = vjo['data']
                url = vList['raw_url']
            if subturl.startswith('http') is False:
                head = re.findall(r"h.*?:", baseurl)[0]
                subturl = head + subturl
            if url.startswith('http') is False:
                head = re.findall(r"h.*?:", baseurl)[0]
                url = head + url
            urlfileName = urllib.parse.quote(fileName)
            subturl = subturl.replace(fileName, urlfileName)
            urlvfileName = urllib.parse.quote(vfileName)
            url = url.replace(vfileName, urlvfileName)
            result['subt'] = subturl
        else:
            if self.ver == '' or self.baseurl == '':
                self.getVersion(id)
            ver = self.ver
            baseurl = self.baseurl
            vfileName = id.replace(baseurl, "")
            vparam = {
                "path": '/' + vfileName,
                "password": "",
                "page_num": 1,
                "page_size": 100
            }
            if ver == 2:
                vrsp = self.postJson(baseurl + 'api/public/path', vparam)
                vjo = json.loads(vrsp.text)
                vList = vjo['data']['files'][0]
                driver = vList['driver']
                url = vList['url']
            elif ver == 3:
                vrsp = self.postJson(baseurl + 'api/fs/get', vparam)
                vjo = json.loads(vrsp.text)
                vList = vjo['data']
                url = vList['raw_url']
                driver = vList['provider']
            if url.startswith('http') is False:
                head = re.findall(r"h.*?:", baseurl)[0]
                url = head + url
            urlvfileName = urllib.parse.quote(vfileName)
            url = url.replace(vfileName, urlvfileName)
            if driver == 'Baidu.Disk':
                result["header"] = {"User-Agent": "pan.baidu.com"}

        result["parse"] = 0
        result["playUrl"] = ''
        result["url"] = url

        return result

    flurl = ''
    config = {
        "player": {},
        "filter": {}
    }
    header = {}

    def localProxy(self, param):
        return [200, "video/MP2T", action, ""]